Revision control
++ // Copyright (C) 2015 basysKom GmbH, opensource@basyskom.com
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtOpcUa/qopcuaargument.h>
++ #include <QtOpcUa/QOpcUaAuthenticationInformation>
++ #include <QtOpcUa/qopcuaaxisinformation.h>
++ #include <QtOpcUa/QOpcUaClient>
++ #include <QtOpcUa/QOpcUaConnectionSettings>
++ #include <QtOpcUa/QOpcUaNode>
++ #include <QtOpcUa/QOpcUaProvider>
++ #include <QtOpcUa/qopcuabinarydataencoding.h>
++ #include <QtOpcUa/QOpcUaHistoryReadResponse>
++ #include <QtOpcUa/QOpcUaLiteralOperand>
++ #include <QtOpcUa/qopcuamultidimensionalarray.h>
++ #include <QtOpcUa/QOpcUaStructureDefinition>
++ #include <QtOpcUa/QOpcUaEnumDefinition>
++ #include <QtOpcUa/qopcuaenumfield.h>
++ #include <QtOpcUa/qopcuaextensionobject.h>
++ #include <QtOpcUa/qopcuacomplexnumber.h>
++ #include <QtOpcUa/qopcuadiagnosticinfo.h>
++ #include <QtOpcUa/qopcuadoublecomplexnumber.h>
++ #include <QtOpcUa/qopcuaeuinformation.h>
++ #include <QtOpcUa/qopcuagenericstructhandler.h>
++ #include <QtOpcUa/qopcuagenericstructvalue.h>
++ #include <QtOpcUa/qopcuaattributeoperand.h>
++ #include <QtOpcUa/qopcuaelementoperand.h>
++ #include <QtOpcUa/qopcuarange.h>
++ #include <QtOpcUa/qopcuastructuredefinition.h>
++ #include <QtOpcUa/qopcuastructurefield.h>
++ #include <QtOpcUa/qopcuaxvalue.h>
++
++ #include <private/qopcuasecuritypolicyuris_p.h>
++
++ #include <QtCore/QCoreApplication>
++ #include <QtCore/QOperatingSystemVersion>
++ #include <QtCore/QElapsedTimer>
++ #include <QtCore/QProcess>
++ #include <QtCore/QScopedPointer>
++ #include <QtCore/QThread>
++ #include <QtCore/QTimer>
++
++ #include <QtTest/QSignalSpy>
++ #include <QTest>
++ #include <QTcpSocket>
++ #include <QTcpServer>
++ #include <QVariantMap>
++
++ using namespace Qt::Literals::StringLiterals;
++
++ const int signalSpyTimeout = 10000;
++
++ class OpcuaConnector
++ {
++ public:
++ OpcuaConnector(QOpcUaClient *client, const QOpcUaEndpointDescription &endPoint)
++ : opcuaClient(client)
++ {
++ QVERIFY(opcuaClient != nullptr);
++ QSignalSpy connectedSpy(opcuaClient, &QOpcUaClient::connected);
++ QSignalSpy disconnectedSpy(opcuaClient, &QOpcUaClient::disconnected);
++ QSignalSpy stateSpy(opcuaClient, &QOpcUaClient::stateChanged);
++
++ opcuaClient->connectToEndpoint(endPoint);
++ stateSpy.wait();
++ if (stateSpy.size() != 2)
++ stateSpy.wait();
++
++ QCOMPARE(connectedSpy.size(), 1); // one connected signal fired
++ QCOMPARE(disconnectedSpy.size(), 0); // zero disconnected signals fired
++ QCOMPARE(stateSpy.size(), 2);
++ QCOMPARE(stateSpy.at(0).at(0), QOpcUaClient::ClientState::Connecting);
++ QCOMPARE(stateSpy.at(1).at(0), QOpcUaClient::ClientState::Connected);
++
++ QCOMPARE(stateSpy.at(0).at(0).value<QOpcUaClient::ClientState>(),
++ QOpcUaClient::ClientState::Connecting);
++ QCOMPARE(stateSpy.at(1).at(0).value<QOpcUaClient::ClientState>(),
++ QOpcUaClient::ClientState::Connected);
++
++ stateSpy.clear();
++ connectedSpy.clear();
++ disconnectedSpy.clear();
++
++ QVERIFY(opcuaClient->endpoint() == endPoint);
++ }
++
++ ~OpcuaConnector()
++ {
++ QSignalSpy connectedSpy(opcuaClient, &QOpcUaClient::connected);
++ QSignalSpy disconnectedSpy(opcuaClient, &QOpcUaClient::disconnected);
++ QSignalSpy stateSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ QVERIFY(opcuaClient != nullptr);
++ if (opcuaClient->state() == QOpcUaClient::Connected) {
++
++ QSignalSpy stateSpy(opcuaClient, &QOpcUaClient::stateChanged);
++
++ opcuaClient->disconnectFromEndpoint();
++ stateSpy.wait(signalSpyTimeout);
++
++ // Once the test has failed, QSignalSpy::wait() returns right away without actually waiting for anything.
++ // Processing events manually satisfies the checks below and prevents all following tests from failing
++ // because of an unexpected initial client state.
++ if (stateSpy.size() < 2) {
++ QElapsedTimer t;
++ t.start();
++ do {
++ QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
++ } while (t.elapsed() < signalSpyTimeout && stateSpy.size() < 2);
++ }
++
++ QVERIFY(opcuaClient->state() == QOpcUaClient::Disconnected);
++
++ QCOMPARE(connectedSpy.size(), 0);
++ QCOMPARE(disconnectedSpy.size(), 1);
++ QCOMPARE(stateSpy.size(), 2);
++ QCOMPARE(stateSpy.at(0).at(0).value<QOpcUaClient::ClientState>(),
++ QOpcUaClient::ClientState::Closing);
++ QCOMPARE(stateSpy.at(1).at(0).value<QOpcUaClient::ClientState>(),
++ QOpcUaClient::ClientState::Disconnected);
++ QCOMPARE(errorSpy.size(), 0);
++ }
++
++ opcuaClient = nullptr;
++ }
++
++ QOpcUaClient *opcuaClient;
++ };
++
++ // clazy:excludeall=non-pod-global-static
++
++ const QString readWriteNode = u"ns=3;s=TestNode.ReadWrite"_s;
++ const QList<QString> xmlElements = {
++ u"<?xml version=\"1\" encoding=\"UTF-8\"?>"_s,
++ u"<?xml version=\"2\" encoding=\"UTF-8\"?>"_s,
++ u"<?xml version=\"3\" encoding=\"UTF-8\"?>"_s};
++ const QList<QOpcUaLocalizedText> localizedTexts = {
++ QOpcUaLocalizedText(u"en"_s, u"English"_s),
++ QOpcUaLocalizedText(u"de"_s, u"German"_s),
++ QOpcUaLocalizedText(u"fr"_s, u"French"_s)};
++ const QList<QOpcUaRange> testRanges = {
++ QOpcUaRange(-100, 100),
++ QOpcUaRange(0, 100),
++ QOpcUaRange(-200, -100)
++ };
++ const QList<QOpcUaEUInformation> testEUInfos = {
++ QOpcUaEUInformation(u"http://www.opcfoundation.org/UA/units/un/cefact"_s,
++ 4408652, QOpcUaLocalizedText(QString(), u"°C"_s), QOpcUaLocalizedText(QString(), u"degree Celsius"_s)),
++ QOpcUaEUInformation(u"http://www.opcfoundation.org/UA/units/un/cefact"_s,
++ 4604232, QOpcUaLocalizedText(QString(), u"°F"_s), QOpcUaLocalizedText(QString(), u"degree Fahrenheit"_s)),
++ QOpcUaEUInformation(u"http://www.opcfoundation.org/UA/units/un/cefact"_s,
++ 5067858, QOpcUaLocalizedText(QString(), u"m"_s), QOpcUaLocalizedText(QString(), u"metre"_s)),
++ };
++ const QList<QOpcUaComplexNumber> testComplex = {
++ QOpcUaComplexNumber(1,2),
++ QOpcUaComplexNumber(-1,2),
++ QOpcUaComplexNumber(1,-2)
++ };
++ const QList<QOpcUaDoubleComplexNumber> testDoubleComplex = {
++ QOpcUaDoubleComplexNumber(1,2),
++ QOpcUaDoubleComplexNumber(-1,2),
++ QOpcUaDoubleComplexNumber(1,-2)
++ };
++ const QList<QOpcUaAxisInformation> testAxisInfo = {
++ QOpcUaAxisInformation(testEUInfos[0], testRanges[0], localizedTexts[0], QOpcUa::AxisScale::Linear, QList<double>({1, 2, 3})),
++ QOpcUaAxisInformation(testEUInfos[1], testRanges[1], localizedTexts[1], QOpcUa::AxisScale::Ln, QList<double>({4, 5, 6})),
++ QOpcUaAxisInformation(testEUInfos[2], testRanges[2], localizedTexts[2], QOpcUa::AxisScale::Log, QList<double>({7, 8, 9}))
++ };
++ const QList<QOpcUaXValue> testXV = {
++ QOpcUaXValue(0, 100),
++ QOpcUaXValue(-10, 100.5),
++ QOpcUaXValue(10, -100.5)
++ };
++ const QList<QUuid> testUuid = {
++ QUuid("e0bd5ccd-f571-4545-9352-61a0f8cb9216"),
++ QUuid("460ebe04-89d8-42f3-a0e0-7b45940f1a4e"),
++ QUuid("72962b91-fa75-4ae6-8d28-b404dc7daf63")
++ };
++ const QList<QString> testNodeId = {
++ u"ns=1;i=42"_s,
++ u"ns=2;s=TestString"_s,
++ u"ns=3;g=72962b91-fa75-4ae6-8d28-b404dc7daf63"_s
++ };
++ const QList<QDateTime> testDateTime = {
++ QDateTime(QDate(1601, 1, 1), QTime(0, 0)),
++ QDateTime(QDate(2014, 01, 23), QTime(21, 0)),
++ QDateTime(QDate(2300, 01, 01), QTime(12, 0)),
++ };
++ const QList<QOpcUa::UaStatusCode> testStatusCode = {
++ QOpcUa::UaStatusCode::Good,
++ QOpcUa::UaStatusCode::BadInvalidArgument,
++ QOpcUa::UaStatusCode::BadNodeIdUnknown
++ };
++ const QList<QOpcUaExpandedNodeId> testExpandedId = {
++ QOpcUaExpandedNodeId(QString(), u"ns=1;i=23"_s),
++ QOpcUaExpandedNodeId(u"MyNamespace"_s, u"ns=2;s=MyNode"_s),
++ QOpcUaExpandedNodeId(u"RemoteNamespace"_s, u"ns=3;b=UXQgZnR3IQ=="_s, 1)
++ };
++ const QList<QOpcUaExpandedNodeId> testExpandedNodeId = {
++ QOpcUaExpandedNodeId(u"namespace1"_s, u"ns=0;i=99"_s, 1),
++ QOpcUaExpandedNodeId(QString(), u"ns=1;i=99"_s),
++ QOpcUaExpandedNodeId(QString(), u"ns=1;s=test"_s)
++ };
++ const QList<QOpcUaArgument> testArguments = {
++ QOpcUaArgument(u"Argument1"_s, u"ns=0;i=12"_s, -1,
++ {},QOpcUaLocalizedText(u"en"_s, u"Description1"_s)),
++ QOpcUaArgument(u"Argument2"_s, u"ns=0;i=12"_s, 2,
++ {2, 2}, QOpcUaLocalizedText(u"en"_s, u"Description2"_s)),
++ QOpcUaArgument(u"Argument3"_s, u"ns=0;i=12"_s, 3,
++ {3, 3, 3}, QOpcUaLocalizedText(u"en"_s, u"Description3"_s))
++ };
++
++ const QByteArray withNull("gh\0i", 4);
++
++ // Test data for DataTypeDefinition types
++
++ QList<QOpcUaStructureField> testStructureFields = {};
++ QList<QOpcUaStructureDefinition> testStructureDefinitions = {};
++ QList<QOpcUaEnumField> testEnumFields = {};
++ QList<QOpcUaEnumDefinition> testEnumDefinitions = {};
++ QList<QOpcUaDiagnosticInfo> testDiagnosticInfos = {};
++ QList<QOpcUaVariant> testVariants = {};
++ QList<QOpcUaDataValue> testDataValues = {};
++
++ void populateDataTypeDefinitionTestData()
++ {
++ QOpcUaStructureField sf1;
++ sf1.setArrayDimensions({1, 2, 3});
++ sf1.setDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Double));
++ sf1.setDescription(QOpcUaLocalizedText(u"en"_s, u"This is a test"_s));
++ sf1.setIsOptional(false);
++ sf1.setMaxStringLength(100);
++ sf1.setName(u"MyDouble"_s);
++ sf1.setValueRank(-1);
++
++ QOpcUaStructureField sf2;
++ sf2.setArrayDimensions({1, 2, 3, 4});
++ sf2.setDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Double));
++ sf2.setDescription(QOpcUaLocalizedText(u"en"_s, u"This is a test"_s));
++ sf2.setIsOptional(true);
++ sf2.setMaxStringLength(100);
++ sf2.setName(u"MyDouble"_s);
++ sf2.setValueRank(0);
++
++ QOpcUaStructureDefinition sd1;
++ sd1.setBaseDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Structure));
++ sd1.setDefaultEncodingId(u"ns=2;i=1234"_s);
++ sd1.setStructureType(QOpcUaStructureDefinition::StructureType::Structure);
++ sd1.setFields({sf1});
++
++ QOpcUaStructureDefinition sd2;
++ sd2.setBaseDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Structure));
++ sd2.setDefaultEncodingId(u"ns=2;i=1235"_s);
++ sd2.setStructureType(QOpcUaStructureDefinition::StructureType::StructureWithOptionalFields);
++ sd2.setFields({sf1, sf2});
++
++ QOpcUaEnumField ef1;
++ ef1.setDescription(QOpcUaLocalizedText(u"en"_s, u"This is a Test"_s));
++ ef1.setDisplayName(QOpcUaLocalizedText(u"en"_s, u"Test"_s));
++ ef1.setName(u"MyOption1"_s);
++ ef1.setValue(0);
++
++ QOpcUaEnumField ef2;
++ ef2.setDescription(QOpcUaLocalizedText(u"en"_s, u"This is a Test"_s));
++ ef2.setDisplayName(QOpcUaLocalizedText(u"en"_s, u"Test2"_s));
++ ef2.setName(u"MyOption2"_s);
++ ef2.setValue(1);
++
++ QOpcUaEnumDefinition ed1;
++ ed1.setFields({ef1});
++
++ QOpcUaEnumDefinition ed2;
++ ed1.setFields({ef1, ef2});
++
++ testStructureFields = { sf1, sf2, sf2 };
++ testStructureDefinitions = { sd1, sd2, sd2 };
++ testEnumFields = { ef1, ef2, ef2 };
++ testEnumDefinitions = { ed1, ed2, ed2 };
++ }
++
++ void populateDiagnosticInfoTestData()
++ {
++ QOpcUaDiagnosticInfo diagnosticInfo;
++ diagnosticInfo.setHasSymbolicId(true);
++ diagnosticInfo.setSymbolicId(1);
++ diagnosticInfo.setHasNamespaceUri(true);
++ diagnosticInfo.setNamespaceUri(2);
++ diagnosticInfo.setHasLocale(true);
++ diagnosticInfo.setLocale(3);
++ diagnosticInfo.setHasLocalizedText(true);
++ diagnosticInfo.setLocalizedText(4);
++ diagnosticInfo.setHasAdditionalInfo(true);
++ diagnosticInfo.setAdditionalInfo(u"My additional info"_s);
++ diagnosticInfo.setHasInnerStatusCode(true);
++ diagnosticInfo.setInnerStatusCode(QOpcUa::UaStatusCode::BadInternalError);
++ diagnosticInfo.setHasInnerDiagnosticInfo(true);
++ diagnosticInfo.innerDiagnosticInfoRef().setHasAdditionalInfo(true);
++ diagnosticInfo.innerDiagnosticInfoRef().setAdditionalInfo(u"My inner additional info"_s);
++
++ QOpcUaDiagnosticInfo diagnosticInfo2;
++ diagnosticInfo2.setHasLocale(true);
++ diagnosticInfo2.setLocale(1);
++ diagnosticInfo2.setHasInnerStatusCode(true);
++ diagnosticInfo2.setInnerStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
++
++ testDiagnosticInfos = { diagnosticInfo, diagnosticInfo2 };
++ }
++
++ void populateVariantTestData()
++ {
++ testVariants = { {QOpcUaVariant::ValueType::String, QList<QString>{ u"Test1"_s, u"Test2"_s, u"Test3"_s, u"Test4"_s}, { 2, 2 }},
++ {QOpcUaVariant::ValueType::Int16, 23}};
++
++ }
++
++ void populateDataValuesTestData()
++ {
++ QOpcUaDataValue d1;
++ d1.setServerTimestamp(QDateTime::currentDateTime());
++ d1.setServerPicoseconds(23);
++ d1.setSourceTimestamp(QDateTime::currentDateTime());
++ d1.setSourcePicoseconds(42);
++ d1.setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
++ d1.setValue(testVariants.at(0));
++
++ testDataValues = { d1 };
++ }
++
++ QList<QOpcUaMonitoringParameters::EventFilter> testEventFilters = {};
++
++ void populateEventFilterTestData()
++ {
++ QOpcUaMonitoringParameters::EventFilter f;
++ f << QOpcUaSimpleAttributeOperand(u"Message"_s);
++ f << QOpcUaSimpleAttributeOperand(u"Severity"_s);
++
++ QOpcUaSimpleAttributeOperand sao(u"Name"_s);
++ sao.setIndexRange(u"0-2"_s);
++
++ const QOpcUaLiteralOperand lo(42, QOpcUa::Types::Int16);
++
++ QOpcUaAttributeOperand ao;
++ ao.setAttributeId(QOpcUa::NodeAttribute::Value);
++ ao.setNodeId(u"ns=0;i=42"_s);
++ ao.setAlias(u"Alias"_s);
++ ao.setBrowsePath({ QOpcUaRelativePathElement(QOpcUaQualifiedName(0, u"Name"_s),
++ QOpcUa::ReferenceTypeId::HasProperty) });
++ ao.setIndexRange(u"0-5"_s);
++
++ const QOpcUaElementOperand eo(2);
++
++ f << (QOpcUaContentFilterElement() << QOpcUaContentFilterElement::FilterOperator::LessThan << sao << lo << ao << eo);
++ f << (QOpcUaContentFilterElement() << QOpcUaContentFilterElement::FilterOperator::LessThan << lo << sao << ao << eo);
++
++ QOpcUaMonitoringParameters::EventFilter f2;
++ f2 << QOpcUaSimpleAttributeOperand(u"Message"_s);
++ f2 << QOpcUaSimpleAttributeOperand(u"Severity"_s);
++
++ f2 << (QOpcUaContentFilterElement() << QOpcUaContentFilterElement::FilterOperator::LessThan << sao << lo << ao << eo);
++ f2 << (QOpcUaContentFilterElement() << QOpcUaContentFilterElement::FilterOperator::LessThan << eo << lo << ao << sao);
++
++ testEventFilters.push_back(f);
++ testEventFilters.push_back(f2);
++ }
++
++
++ #define ENCODE_EXTENSION_OBJECT(obj, index) \
++ { \
++ QVERIFY(index < 3); \
++ obj.setEncoding(QOpcUaExtensionObject::Encoding::ByteString); \
++ obj.setEncodingTypeId(u"ns=2;s=MyEncoding%1"_s.arg(index)); \
++ QOpcUaBinaryDataEncoding encoder(obj); \
++ encoder.encode<quint8>(quint8(index)); \
++ encoder.encode<qint8>(qint8(index)); \
++ encoder.encode<quint16>(quint16(index)); \
++ encoder.encode<qint16>(qint16(index)); \
++ encoder.encode<quint32>(quint32(index)); \
++ encoder.encode<qint32>(qint32(index)); \
++ encoder.encode<quint64>(quint64(index)); \
++ encoder.encode<qint64>(qint64(index)); \
++ encoder.encode<float>(float(index)); \
++ encoder.encode<double>(double(index)); \
++ encoder.encode<bool>(bool(index)); \
++ encoder.encode<QString>(u"String %1"_s.arg(index)); \
++ encoder.encode<QOpcUaQualifiedName>(QOpcUaQualifiedName(2, u"QualifiedName %1"_s.arg(index))); \
++ encoder.encode<QOpcUaLocalizedText>(localizedTexts.at(index)); \
++ encoder.encode<QOpcUaRange>(testRanges.at(index)); \
++ encoder.encode<QOpcUaEUInformation>(testEUInfos.at(index)); \
++ encoder.encode<QOpcUaComplexNumber>(testComplex.at(index)); \
++ encoder.encode<QOpcUaDoubleComplexNumber>(testDoubleComplex.at(index)); \
++ encoder.encode<QOpcUaAxisInformation>(testAxisInfo.at(index)); \
++ encoder.encode<QOpcUaXValue>(testXV.at(index)); \
++ encoder.encode<QUuid>(testUuid.at(index)); \
++ encoder.encode<QString, QOpcUa::Types::NodeId>(testNodeId.at(index)); \
++ encoder.encode<QDateTime>(testDateTime.at(index)); \
++ encoder.encode<QOpcUa::UaStatusCode>(testStatusCode.at(index)); \
++ encoder.encode<QOpcUaExpandedNodeId>(testExpandedId.at(index)); \
++ QOpcUaExtensionObject ext; \
++ ext.setEncodingTypeId(obj.encodingTypeId()); \
++ ext.setEncoding(obj.encoding()); \
++ QOpcUaBinaryDataEncoding encoding2(&ext.encodedBodyRef()); \
++ encoding2.encode<QString>(u"String %1"_s.arg(index)); \
++ encoder.encode<QOpcUaExtensionObject>(ext); \
++ encoder.encode<QOpcUaArgument>(testArguments.at(index)); \
++ encoder.encode<QOpcUaStructureDefinition>(testStructureDefinitions.at(index)); \
++ encoder.encode<QOpcUaEnumDefinition>(testEnumDefinitions.at(index)); \
++ }
++
++ #define VERIFY_EXTENSION_OBJECT(obj, index) \
++ { \
++ QVERIFY(index < 3); \
++ QCOMPARE(obj.encoding(), QOpcUaExtensionObject::Encoding::ByteString); \
++ QCOMPARE(obj.encodingTypeId(), u"ns=2;s=MyEncoding%1"_s.arg(index)); \
++ bool success = false; \
++ QOpcUaBinaryDataEncoding decoder(obj); \
++ QCOMPARE(decoder.offset(), 0); \
++ QCOMPARE(decoder.decode<quint8>(success), quint8(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<qint8>(success), qint8(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<quint16>(success), quint16(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<qint16>(success), qint16(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<quint32>(success), quint32(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<qint32>(success), qint32(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<quint64>(success), quint64(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<qint64>(success), qint64(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<float>(success), float(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<double>(success), double(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<bool>(success), bool(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QString>(success), u"String %1"_s.arg(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaQualifiedName>(success), \
++ QOpcUaQualifiedName(2, u"QualifiedName %1"_s.arg(index))); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaLocalizedText>(success), localizedTexts.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaRange>(success), testRanges.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaEUInformation>(success), testEUInfos.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaComplexNumber>(success), testComplex.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaDoubleComplexNumber>(success), testDoubleComplex.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaAxisInformation>(success), testAxisInfo.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaXValue>(success), testXV.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QUuid>(success), testUuid.at(index)); \
++ QVERIFY(success == true); \
++ QString temp = decoder.decode<QString, QOpcUa::Types::NodeId>(success); \
++ QCOMPARE(temp, testNodeId.at(index)); \
++ QVERIFY(success == true); \
++ QDateTime dt = decoder.decode<QDateTime>(success); \
++ if (index == 0) \
++ QCOMPARE(dt, QDateTime()); \
++ else \
++ QCOMPARE(dt, testDateTime.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUa::UaStatusCode>(success), testStatusCode.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaExpandedNodeId>(success), testExpandedId.at(index)); \
++ QVERIFY(success == true); \
++ QOpcUaExtensionObject ext = decoder.decode<QOpcUaExtensionObject>(success); \
++ QCOMPARE(ext.encodingTypeId(), obj.encodingTypeId()); \
++ QCOMPARE(ext.encoding(), obj.encoding()); \
++ QVERIFY(success == true); \
++ QOpcUaBinaryDataEncoding decoder2(&ext.encodedBodyRef()); \
++ QCOMPARE(decoder2.decode<QString>(success), u"String %1"_s.arg(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaArgument>(success), testArguments.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaStructureDefinition>(success), testStructureDefinitions.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.decode<QOpcUaEnumDefinition>(success), testEnumDefinitions.at(index)); \
++ QVERIFY(success == true); \
++ QCOMPARE(decoder.offset(), obj.encodedBody().size()); \
++ }
++
++ #define defineDataMethod(name) void name()\
++ {\
++ QTest::addColumn<QOpcUaClient *>("opcuaClient");\
++ for (auto *client : std::as_const(m_clients))\
++ QTest::newRow(client->backend().toLatin1().constData()) << client;\
++ }
++
++ class Tst_QOpcUaClient: public QObject
++ {
++ Q_OBJECT
++
++ public:
++ Tst_QOpcUaClient();
++
++ private slots:
++ void initTestCase();
++ void cleanupTestCase();
++
++ void loadInvalidPlugin();
++
++ // connect with invalid configuration
++ defineDataMethod(connectToInvalid_data)
++ void connectToInvalid();
++ defineDataMethod(connectWithEmptyUrl_data)
++ void connectWithEmptyUrl();
++ defineDataMethod(connectWithMalformedUrl_data)
++ void connectWithMalformedUrl();
++ defineDataMethod(connectWithoutTokens_data);
++ void connectWithoutTokens();
++ defineDataMethod(connectWithInvalidSecurityMode_data);
++ void connectWithInvalidSecurityMode();
++ defineDataMethod(connectWithInvalidSecurityPolicyUri_data);
++ void connectWithInvalidSecurityPolicyUri();
++ defineDataMethod(connectSecureEndpointWithoutPki_data);
++ void connectSecureEndpointWithoutPki();
++ defineDataMethod(connectEncryptedTokenWithoutPki_data);
++ void connectEncryptedTokenWithoutPki();
++ defineDataMethod(connectCertificateAuthWithoutPki_data);
++ void connectCertificateAuthWithoutPki();
++
++ // connect & disconnect
++ defineDataMethod(connectAndDisconnect_data)
++ void connectAndDisconnect();
++ defineDataMethod(checkSessionLocaleIds_data)
++ void checkSessionLocaleIds();
++ defineDataMethod(connectionSettings_data)
++ void connectionSettings();
++
++ // Password
++ defineDataMethod(connectInvalidPassword_data)
++ void connectInvalidPassword();
++ defineDataMethod(connectAndDisconnectPassword_data)
++ void connectAndDisconnectPassword();
++
++ // Server discovery
++ defineDataMethod(findServers_data)
++ void findServers();
++
++ // Endpoint discovery
++ defineDataMethod(requestEndpoints_data)
++ void requestEndpoints();
++
++ defineDataMethod(compareNodeIds_data)
++ void compareNodeIds();
++ defineDataMethod(getNodeForIdTypes_data)
++ void getNodeForIdTypes();
++ defineDataMethod(readNodeWithByteStringIdWithNullByte_data)
++ void readNodeWithByteStringIdWithNullByte();
++ defineDataMethod(readNS0OmitNode_data)
++ void readNS0OmitNode();
++ defineDataMethod(readInvalidNode_data)
++ void readInvalidNode();
++ defineDataMethod(requestNotInCache_data)
++ void requestNotInCache();
++ defineDataMethod(writeInvalidNode_data)
++ void writeInvalidNode();
++ defineDataMethod(writeMultipleAttributes_data)
++ void writeMultipleAttributes();
++ defineDataMethod(readEmptyArrayVariable_data)
++ void readEmptyArrayVariable();
++ defineDataMethod(writeReadNodeAttributes_data)
++ void writeReadNodeAttributes();
++
++ defineDataMethod(readDataTypeDefinition_data)
++ void readDataTypeDefinition();
++ defineDataMethod(readAccessLevelEx_data)
++ void readAccessLevelEx();
++ defineDataMethod(readNewPermissionAttributes_data)
++ void readNewPermissionAttributes();
++
++ defineDataMethod(getRootNode_data)
++ void getRootNode();
++ defineDataMethod(getChildren_data)
++ void getChildren();
++ defineDataMethod(childrenIdsString_data)
++ void childrenIdsString();
++ defineDataMethod(childrenIdsGuidNodeId_data)
++ void childrenIdsGuidNodeId();
++ defineDataMethod(childrenIdsOpaqueNodeId_data)
++ void childrenIdsOpaqueNodeId();
++ defineDataMethod(testSpecialCharStringNodeIds_data)
++ void testSpecialCharStringNodeIds();
++ defineDataMethod(inverseBrowse_data)
++ void inverseBrowse();
++
++ defineDataMethod(addAndRemoveObjectNode_data)
++ void addAndRemoveObjectNode();
++ defineDataMethod(addAndRemoveVariableNode_data)
++ void addAndRemoveVariableNode();
++ defineDataMethod(addAndRemoveMethodNode_data)
++ void addAndRemoveMethodNode();
++ defineDataMethod(addAndRemoveObjectTypeNode_data)
++ void addAndRemoveObjectTypeNode();
++ defineDataMethod(addAndRemoveVariableTypeNode_data)
++ void addAndRemoveVariableTypeNode();
++ defineDataMethod(addAndRemoveReferenceTypeNode_data)
++ void addAndRemoveReferenceTypeNode();
++ defineDataMethod(addAndRemoveDataTypeNode_data)
++ void addAndRemoveDataTypeNode();
++ defineDataMethod(addAndRemoveViewNode_data)
++ void addAndRemoveViewNode();
++ defineDataMethod(addAndRemoveReference_data)
++ void addAndRemoveReference();
++
++ defineDataMethod(dataChangeSubscription_data)
++ void dataChangeSubscription();
++ defineDataMethod(dataChangeSubscriptionInvalidNode_data)
++ void dataChangeSubscriptionInvalidNode();
++ defineDataMethod(dataChangeSubscriptionSharing_data)
++ void dataChangeSubscriptionSharing();
++ defineDataMethod(methodCall_data)
++ void dataChangeSubscriptionTriggering();
++ defineDataMethod(dataChangeSubscriptionTriggering_data);
++ void dataChangeSubscriptionModifyTriggering();
++ defineDataMethod(dataChangeSubscriptionModifyTriggering_data);
++ void methodCall();
++ defineDataMethod(methodCallInvalid_data)
++ void methodCallInvalid();
++ defineDataMethod(readMethodArguments_data)
++ void readMethodArguments();
++ defineDataMethod(malformedNodeString_data)
++ void malformedNodeString();
++ defineDataMethod(nodeIdGeneration_data)
++ void nodeIdGeneration();
++
++ defineDataMethod(multipleClients_data)
++ void multipleClients();
++ defineDataMethod(nodeClass_data)
++ void nodeClass();
++ defineDataMethod(writeReadArray_data)
++ void writeReadArray();
++ defineDataMethod(writeReadScalar_data)
++ void writeReadScalar();
++ defineDataMethod(readReencodedExtensionObject_data)
++ void readReencodedExtensionObject();
++ defineDataMethod(binaryDataEncodingRecursionDepth_data)
++ void binaryDataEncodingRecursionDepth();
++ defineDataMethod(indexRange_data)
++ void indexRange();
++ defineDataMethod(invalidIndexRange_data)
++ void invalidIndexRange();
++ defineDataMethod(subscriptionIndexRange_data)
++ void subscriptionIndexRange();
++ defineDataMethod(subscriptionDataChangeFilter_data)
++ void subscriptionDataChangeFilter();
++ defineDataMethod(modifyPublishingMode_data)
++ void modifyPublishingMode();
++ defineDataMethod(modifyMonitoringMode_data)
++ void modifyMonitoringMode();
++ defineDataMethod(modifyMonitoredItem_data)
++ void modifyMonitoredItem();
++ defineDataMethod(addDuplicateMonitoredItem_data)
++ void addDuplicateMonitoredItem();
++ defineDataMethod(subscriptionUnreadableNode_data);
++ void subscriptionUnreadableNode();
++ defineDataMethod(checkMonitoredItemCleanup_data);
++ void checkMonitoredItemCleanup();
++ defineDataMethod(checkAttributeUpdated_data);
++ void checkAttributeUpdated();
++ defineDataMethod(checkValueAttributeUpdated_data);
++ void checkValueAttributeUpdated();
++ defineDataMethod(checkMonitoringInvalidRequests_data);
++ void checkMonitoringInvalidRequests();
++
++ defineDataMethod(stringCharset_data)
++ void stringCharset();
++
++ defineDataMethod(namespaceArray_data)
++ void namespaceArray();
++
++ defineDataMethod(multiDimensionalArray_data)
++ void multiDimensionalArray();
++
++ defineDataMethod(dateTimeConversion_data)
++ void dateTimeConversion();
++ defineDataMethod(timeStamps_data)
++ void timeStamps();
++
++ defineDataMethod(createNodeFromExpandedId_data)
++ void createNodeFromExpandedId();
++ defineDataMethod(checkExpandedIdConversion_data)
++ void checkExpandedIdConversion();
++ defineDataMethod(checkExpandedIdConversionNoOk_data)
++ void checkExpandedIdConversionNoOk();
++ defineDataMethod(createQualifiedName_data)
++ void createQualifiedName();
++ defineDataMethod(createQualifiedNameNoOk_data)
++ void createQualifiedNameNoOk();
++ defineDataMethod(addNamespace_data)
++ void addNamespace();
++
++ void fixedTimestamp();
++ defineDataMethod(fixedTimestamp_data)
++
++ defineDataMethod(resolveBrowsePath_data)
++ void resolveBrowsePath();
++
++ defineDataMethod(extensionObjectWithGuid_data)
++ void extensionObjectWithGuid();
++
++ defineDataMethod(encodeEmptyStringNodeId_data)
++ void encodeEmptyStringNodeId();
++
++ void statusStrings();
++
++ defineDataMethod(readHistoryDataFromNode_data)
++ void readHistoryDataFromNode();
++
++ defineDataMethod(readHistoryDataFromClient_data)
++ void readHistoryDataFromClient();
++
++ defineDataMethod(readHistoryEventsFromNode_data)
++ void readHistoryEventsFromNode();
++
++ defineDataMethod(readHistoryEventsFromClient_data)
++ void readHistoryEventsFromClient();
++
++ // Events
++ defineDataMethod(eventSubscription_data)
++ void eventSubscription();
++
++ defineDataMethod(decodeGenericStruct_data)
++ void decodeGenericStruct();
++
++ defineDataMethod(encodeGenericStruct_data)
++ void encodeGenericStruct();
++
++ defineDataMethod(encodeCustomGenericStruct_data)
++ void encodeCustomGenericStruct();
++
++ defineDataMethod(registerUnregisterNodes_data)
++ void registerUnregisterNodes();
++
++ // This test case restarts the server. It must be run last to avoid
++ // destroying state required by other test cases.
++ defineDataMethod(connectionLost_data)
++ void connectionLost();
++
++ private:
++ QString envOrDefault(const char *env, QString def)
++ {
++ return qEnvironmentVariableIsSet(env) ? QString::fromUtf8(qgetenv(env).constData()) : def;
++ }
++
++ QUrl m_discoveryEndpoint;
++ QOpcUaProvider m_opcUa;
++ QStringList m_backends;
++ QList<QOpcUaClient *> m_clients;
++ QProcess m_serverProcess;
++ QString m_testServerPath;
++ QOpcUaEndpointDescription m_endpoint;
++ };
++
++ #define READ_MANDATORY_BASE_NODE(NODE) \
++ { \
++ QSignalSpy attributeReadSpy(NODE.data(), &QOpcUaNode::attributeRead);\
++ NODE->readAttributes(QOpcUaNode::mandatoryBaseAttributes()); \
++ attributeReadSpy.wait(signalSpyTimeout); \
++ QCOMPARE(attributeReadSpy.count(), 1); \
++ QCOMPARE(attributeReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>(), QOpcUaNode::mandatoryBaseAttributes()); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::NodeId)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::NodeClass)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::BrowseName)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::DisplayName)) == true); \
++ }
++
++ #define READ_MANDATORY_VARIABLE_NODE(NODE) \
++ { \
++ QSignalSpy attributeReadSpy(NODE.data(), &QOpcUaNode::attributeRead);\
++ NODE->readAttributes(QOpcUaNode::mandatoryBaseAttributes() | QOpcUa::NodeAttribute::Value); \
++ attributeReadSpy.wait(signalSpyTimeout); \
++ QCOMPARE(attributeReadSpy.count(), 1); \
++ QCOMPARE(attributeReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>(), (QOpcUaNode::mandatoryBaseAttributes() | QOpcUa::NodeAttribute::Value)); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::NodeId)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::NodeClass)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::BrowseName)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::DisplayName)) == true); \
++ QVERIFY(QOpcUa::isSuccessStatus(NODE->attributeError(QOpcUa::NodeAttribute::Value)) == true); \
++ }
++
++ #define WRITE_VALUE_ATTRIBUTE(NODE, VALUE, TYPE) \
++ { \
++ QSignalSpy resultSpy(NODE.data(), &QOpcUaNode::attributeWritten); \
++ NODE->writeAttribute(QOpcUa::NodeAttribute::Value, VALUE, TYPE); \
++ resultSpy.wait(signalSpyTimeout); \
++ QCOMPARE(resultSpy.size(), 1); \
++ QCOMPARE(resultSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value); \
++ QCOMPARE(resultSpy.at(0).at(1), QOpcUa::UaStatusCode::Good); \
++ }
++
++ Tst_QOpcUaClient::Tst_QOpcUaClient()
++ {
++ m_backends = QOpcUaProvider::availableBackends();
++ }
++
++ void Tst_QOpcUaClient::initTestCase()
++ {
++ #if defined(Q_OS_MACOS)
++ const bool runsOnCI = qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci");
++ const auto osVer = QOperatingSystemVersion::current();
++ if (runsOnCI && osVer >= QOperatingSystemVersion::MacOSTahoe)
++ QSKIP("The tests fails on macOS 26 in CI: QTBUG-139354");
++ #endif
++
++ const quint16 defaultPort = 43344;
++ const QHostAddress defaultHost(QHostAddress::LocalHost);
++
++ for (const auto &backend: std::as_const(m_backends)) {
++ QVariantMap backendOptions;
++
++ QOpcUaClient *client = m_opcUa.createClient(backend, backendOptions);
++ QVERIFY2(client != nullptr,
++ u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
++ client->setParent(this);
++ qDebug() << "Using SDK plugin:" << client->backend();
++ m_clients.append(client);
++ }
++
++ if (qEnvironmentVariableIsEmpty("OPCUA_HOST") && qEnvironmentVariableIsEmpty("OPCUA_PORT")) {
++ m_testServerPath = qApp->applicationDirPath()
++
++ #if defined(Q_OS_MACOS)
++ + "/../../open62541-testserver/open62541-testserver.app/Contents/MacOS/open62541-testserver"_L1
++ #else
++
++ #if defined(Q_OS_WIN) && !defined(TESTS_CMAKE_SPECIFIC_PATH)
++ + "/.."_L1
++ #endif
++ + "/../../open62541-testserver/open62541-testserver"_L1
++ #ifdef Q_OS_WIN
++ + ".exe"_L1
++ #endif
++
++ #endif
++ ;
++ if (!QFile::exists(m_testServerPath)) {
++ qDebug() << "Server Path:" << m_testServerPath;
++ QSKIP("all auto tests rely on an open62541-based test-server");
++ }
++
++ // In this case the test is supposed to open its own server.
++ // Unfortunately there is no way to check if the server has started up successfully
++ // because of missing error handling.
++ // This checks will detect other servers blocking the port.
++
++ // Check for running server
++ QTcpSocket socket;
++ socket.connectToHost(defaultHost, defaultPort);
++ QVERIFY2(socket.waitForConnected(1500) == false, "Server is already running");
++
++ // Check for running server which does not respond
++ QTcpServer server;
++ QVERIFY2(server.listen(defaultHost, defaultPort) == true, "Port is occupied by another process. Check for defunct server.");
++ server.close();
++
++ m_serverProcess.start(m_testServerPath);
++ QVERIFY2(m_serverProcess.waitForStarted(), qPrintable(m_serverProcess.errorString()));
++
++ // Check if server is up and running
++ QVERIFY(m_serverProcess.state() == QProcess::Running);
++
++ QTest::qSleep(100);
++ socket.connectToHost(defaultHost, defaultPort);
++ if (!socket.waitForConnected(5000))
++ {
++ bool success = false;
++ for (int i = 0; i < 50; ++i) {
++ QTest::qSleep(100);
++ socket.connectToHost(defaultHost, defaultPort);
++ if (socket.waitForConnected(5000)) {
++ success = true;
++ break;
++ }
++ }
++
++ if (!success)
++ QFAIL("Server does not run");
++ }
++
++ socket.disconnectFromHost();
++ }
++ QString host = envOrDefault("OPCUA_HOST", defaultHost.toString());
++ QString port = envOrDefault("OPCUA_PORT", QString::number(defaultPort));
++ m_discoveryEndpoint.setUrl("opc.tcp://%1:%2"_L1.arg(host, port));
++ qDebug() << "Using endpoint:" << m_discoveryEndpoint;
++
++ QOpcUaClient *client = m_clients.first();
++ if (client) {
++ QSignalSpy endpointSpy(m_clients.first(), &QOpcUaClient::endpointsRequestFinished);
++
++ client->requestEndpoints(m_discoveryEndpoint);
++ endpointSpy.wait(signalSpyTimeout);
++ QCOMPARE(endpointSpy.size(), 1);
++
++ const auto desc = endpointSpy.at(0).at(0).value<QList<QOpcUaEndpointDescription>>();
++ QVERIFY(desc.size() > 0);
++ QCOMPARE(endpointSpy.at(0).at(2).value<QUrl>(), m_discoveryEndpoint);
++
++ m_endpoint = desc.first();
++ }
++ }
++
++ void Tst_QOpcUaClient::connectToInvalid()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ QOpcUaEndpointDescription invalidEndpoint;
++ invalidEndpoint.setEndpointUrl(u"opc.tcp:127.0.0.1:1234"_s);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++
++ opcuaClient->connectToEndpoint(invalidEndpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(opcuaClient->endpoint(), invalidEndpoint);
++ }
++
++ void Tst_QOpcUaClient::connectWithEmptyUrl()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setEndpointUrl({});
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidUrl);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::InvalidUrl);
++ }
++
++ void Tst_QOpcUaClient::connectWithMalformedUrl()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setEndpointUrl(u"asdf://127.0.0.1"_s);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidUrl);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::InvalidUrl);
++ }
++
++ void Tst_QOpcUaClient::connectWithoutTokens()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setUserIdentityTokens({});
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
++ }
++
++ void Tst_QOpcUaClient::connectWithInvalidSecurityMode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setSecurityMode(QOpcUaEndpointDescription::MessageSecurityMode::Invalid);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidEndpointDescription);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::InvalidEndpointDescription);
++ }
++
++ void Tst_QOpcUaClient::connectWithInvalidSecurityPolicyUri()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setSecurityPolicy("http://notavalidsecuritypolicyuri"_L1);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::UnsupportedSecurityPolicy);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::UnsupportedSecurityPolicy);
++ }
++
++ void Tst_QOpcUaClient::connectSecureEndpointWithoutPki()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ endpoint.setSecurityPolicy(QOpcUa::Basic256Sha256Policy);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::UnsupportedSecurityPolicy);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::UnsupportedSecurityPolicy);
++ }
++
++ void Tst_QOpcUaClient::connectEncryptedTokenWithoutPki()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ auto endpoint = m_endpoint;
++ auto tokens = endpoint.userIdentityTokens();
++ for (auto &entry : tokens)
++ entry.setSecurityPolicy(QOpcUa::Basic256Sha256Policy);
++ endpoint.setUserIdentityTokens(tokens);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(opcuaClient, &QOpcUaClient::errorChanged);
++
++ opcuaClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::Disconnected);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
++ }
++
++ void Tst_QOpcUaClient::connectCertificateAuthWithoutPki()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Use a new client for the test to avoid keeping the auth information
++ QScopedPointer<QOpcUaClient> tempClient(m_opcUa.createClient(opcuaClient->backend()));
++ QVERIFY(tempClient);
++
++ auto endpoint = m_endpoint;
++ QOpcUaAuthenticationInformation authInfo;
++ authInfo.setCertificateAuthentication();
++ tempClient->setAuthenticationInformation(authInfo);
++
++ QSignalSpy connectSpy(tempClient.get(), &QOpcUaClient::stateChanged);
++ QSignalSpy errorSpy(tempClient.get(), &QOpcUaClient::errorChanged);
++
++ tempClient->connectToEndpoint(endpoint);
++
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++
++ if (errorSpy.isEmpty())
++ errorSpy.wait();
++
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(tempClient->state(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(errorSpy.size(), 1);
++ #ifdef SERVER_SUPPORTS_SECURITY
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidAuthenticationInformation);
++ QCOMPARE(tempClient->error(), QOpcUaClient::ClientError::InvalidAuthenticationInformation);
++ #else
++ QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
++ QCOMPARE(tempClient->error(), QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
++ #endif
++ }
++
++ void Tst_QOpcUaClient::connectAndDisconnect()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++ }
++
++ void Tst_QOpcUaClient::checkSessionLocaleIds()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Empty locale id => fallback to en
++ {
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"en"_s, u"Hello"_s));
++ }
++
++ // Invalid locale id => fallback to en
++ {
++ QOpcUaConnectionSettings settings;
++ settings.setSessionLocaleIds({ u"xx"_s });
++ opcuaClient->setConnectionSettings(settings);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"en"_s, u"Hello"_s));
++ }
++
++ // German locale id (short)
++ {
++ QOpcUaConnectionSettings settings;
++ settings.setSessionLocaleIds({ u"de"_s });
++ opcuaClient->setConnectionSettings(settings);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"de"_s, u"Guten Tag"_s));
++ }
++
++ // German locale id
++ {
++ QOpcUaConnectionSettings settings;
++ settings.setSessionLocaleIds({ u"de-DE"_s });
++ opcuaClient->setConnectionSettings(settings);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"de"_s, u"Guten Tag"_s));
++ }
++
++ // French locale id (short)
++ {
++ QOpcUaConnectionSettings settings;
++ settings.setSessionLocaleIds({ u"fr"_s });
++ opcuaClient->setConnectionSettings(settings);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"fr"_s, u"Bonjour"_s));
++ }
++
++ // French locale id
++ {
++ QOpcUaConnectionSettings settings;
++ settings.setSessionLocaleIds({ u"fr-FR"_s });
++ opcuaClient->setConnectionSettings(settings);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"fr"_s, u"Bonjour"_s));
++ }
++ }
++
++ void Tst_QOpcUaClient::connectionSettings()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QOpcUaConnectionSettings resetSettings;
++ resetSettings.setSessionLocaleIds({ u"en"_s });
++ opcuaClient->setConnectionSettings(resetSettings);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=LocalizedTextWithCallback"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"en"_s, u"Hello"_s));
++
++ auto settings = opcuaClient->connectionSettings();
++ settings.setSessionLocaleIds({ u"de"_s });
++ opcuaClient->setConnectionSettings(settings);
++
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->valueAttribute(), QOpcUaLocalizedText(u"de"_s, u"Guten Tag"_s));
++ }
++
++ void Tst_QOpcUaClient::connectInvalidPassword()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QOpcUaAuthenticationInformation authInfo;
++ authInfo.setUsernameAuthentication(u"invaliduser"_s, u"wrongpassword"_s);
++ opcuaClient->setAuthenticationInformation(authInfo);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++
++ opcuaClient->connectToEndpoint(m_endpoint);
++ connectSpy.wait();
++ if (connectSpy.size() < 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++
++ QCOMPARE(opcuaClient->endpoint(), m_endpoint);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::AccessDenied);
++ }
++
++ void Tst_QOpcUaClient::connectAndDisconnectPassword()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QOpcUaAuthenticationInformation authInfo;
++ authInfo.setUsernameAuthentication(u"user1"_s, u"password"_s);
++ opcuaClient->setAuthenticationInformation(authInfo);
++
++ QSignalSpy connectSpy(opcuaClient, &QOpcUaClient::stateChanged);
++
++ opcuaClient->connectToEndpoint(m_endpoint);
++ connectSpy.wait(signalSpyTimeout);
++ if (connectSpy.size() != 2)
++ connectSpy.wait();
++
++ QCOMPARE(connectSpy.size(), 2);
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
++
++ QCOMPARE(opcuaClient->endpoint(), m_endpoint);
++ QCOMPARE(opcuaClient->error(), QOpcUaClient::NoError);
++
++ connectSpy.clear();
++ opcuaClient->disconnectFromEndpoint();
++ connectSpy.wait(signalSpyTimeout);
++ if (connectSpy.size() != 2)
++ connectSpy.wait();
++ QCOMPARE(connectSpy.size(), 2);
++ QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
++ QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
++ }
++
++ void Tst_QOpcUaClient::findServers()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QSignalSpy discoverySpy(opcuaClient, &QOpcUaClient::findServersFinished);
++
++ opcuaClient->findServers(m_discoveryEndpoint);
++
++ discoverySpy.wait(signalSpyTimeout);
++
++ QCOMPARE(discoverySpy.size(), 1);
++ QCOMPARE(discoverySpy.at(0).at(2).value<QUrl>(), m_discoveryEndpoint);
++
++ QCOMPARE(discoverySpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto servers = discoverySpy.at(0).at(0).value<QList<QOpcUaApplicationDescription>>();
++ QCOMPARE(servers.size(), 1);
++
++ QCOMPARE(servers.at(0).applicationName(), QOpcUaLocalizedText(u"en"_s, u"open62541-based OPC UA Application"_s));
++ QCOMPARE(servers.at(0).applicationUri(), u"urn:open62541.server.application"_s);
++ QVERIFY(servers.at(0).discoveryUrls().size() >= 1);
++ }
++
++ void Tst_QOpcUaClient::requestEndpoints()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QSignalSpy endpointSpy(opcuaClient, &QOpcUaClient::endpointsRequestFinished);
++
++ opcuaClient->requestEndpoints(m_discoveryEndpoint);
++ endpointSpy.wait(signalSpyTimeout);
++ QCOMPARE(endpointSpy.size(), 1);
++ QCOMPARE(endpointSpy.at(0).at(2).value<QUrl>(), m_discoveryEndpoint);
++
++ auto desc = endpointSpy.at(0).at(0).value<QList<QOpcUaEndpointDescription>>();
++ QVERIFY(desc.size() > 0);
++
++ QCOMPARE(QUrl(desc[0].endpointUrl()).port(), 43344);
++ QCOMPARE(desc[0].securityPolicy(), u"http://opcfoundation.org/UA/SecurityPolicy#None"_s);
++ QCOMPARE(desc[0].transportProfileUri(), u"http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary"_s);
++ QCOMPARE(desc[0].securityLevel(), 0);
++ QCOMPARE(desc[0].securityMode(), QOpcUaEndpointDescription::MessageSecurityMode::None);
++ #ifdef SERVER_SUPPORTS_SECURITY
++ QFile file(u":/open62541-testserver/pki/own/certs/open62541-testserver.der"_s);
++ QVERIFY(file.open(QFile::ReadOnly));
++ const auto serverCertificate = file.readAll();
++ QVERIFY(serverCertificate.size() > 0);
++ file.close();
++ QCOMPARE(desc[0].serverCertificate(), serverCertificate);
++ #else
++ QCOMPARE(desc[0].serverCertificate(), QByteArray());
++ #endif
++
++ #ifdef SERVER_SUPPORTS_SECURITY
++ const int numTokensExpected = opcuaClient->supportedSecurityPolicies()
++ .contains(u"http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15"_s)
++ ? 18 : 12;
++ QCOMPARE(desc[0].userIdentityTokens().size(), numTokensExpected);
++ #else
++ QCOMPARE(desc[0].userIdentityTokens().size(), 6);
++ #endif
++ QCOMPARE(desc[0].userIdentityTokens()[0].policyId(), u"open62541-anonymous-policy-none#None"_s);
++ QCOMPARE(desc[0].userIdentityTokens()[0].tokenType(), QOpcUaUserTokenPolicy::TokenType::Anonymous);
++
++ #ifdef SERVER_SUPPORTS_SECURITY
++ QCOMPARE(desc[0].userIdentityTokens()[1].policyId(), u"open62541-certificate-policy-none#Basic256Sha256"_s);
++ QCOMPARE(desc[0].userIdentityTokens()[1].tokenType(), QOpcUaUserTokenPolicy::TokenType::Certificate);
++ QCOMPARE(desc[0].userIdentityTokens()[2].policyId(), u"open62541-username-policy-none#None"_s);
++ QCOMPARE(desc[0].userIdentityTokens()[2].tokenType(), QOpcUaUserTokenPolicy::TokenType::Username);
++ #endif
++
++ QCOMPARE(desc[0].serverRef().applicationName().text(), u"open62541-based OPC UA Application"_s);
++ QCOMPARE(desc[0].serverRef().applicationType(), QOpcUaApplicationDescription::ApplicationType::Server);
++ QCOMPARE(desc[0].serverRef().applicationUri(), u"urn:open62541.server.application"_s);
++ QCOMPARE(desc[0].serverRef().productUri(), u"http://open62541.org"_s);
++ }
++
++ void Tst_QOpcUaClient::compareNodeIds()
++ {
++ const QString numericId = u"i=42"_s;
++ const QString stringId = u"s=TestString"_s;
++ const QString guidId = u"g=72962b91-fa75-4ae6-8d28-b404dc7daf63"_s;
++ const QString opaqueId = u"b=UXQgZnR3IQ=="_s;
++
++ const QString prefix = u"ns=0;"_s;
++
++ QVERIFY(QOpcUa::nodeIdEquals(numericId, prefix + numericId));
++ QVERIFY(QOpcUa::nodeIdEquals(stringId, prefix + stringId));
++ QVERIFY(QOpcUa::nodeIdEquals(guidId, prefix + guidId));
++ QVERIFY(QOpcUa::nodeIdEquals(opaqueId, prefix + opaqueId));
++
++ {
++ quint16 namespaceIndex = 1;
++ char identifierType = 0;
++ QString identifier;
++ QVERIFY(QOpcUa::nodeIdStringSplit(numericId, &namespaceIndex, &identifier, &identifierType));
++ QCOMPARE(namespaceIndex, 0);
++ QCOMPARE(identifierType, 'i');
++ QCOMPARE(identifier, u"42"_s);
++ }
++ {
++ quint16 namespaceIndex = 1;
++ char identifierType = 0;
++ QString identifier;
++ QVERIFY(QOpcUa::nodeIdStringSplit(stringId, &namespaceIndex, &identifier, &identifierType));
++ QCOMPARE(namespaceIndex, 0);
++ QCOMPARE(identifierType, 's');
++ QCOMPARE(identifier, u"TestString"_s);
++ }
++ {
++ quint16 namespaceIndex = 1;
++ char identifierType = 0;
++ QString identifier;
++ QVERIFY(QOpcUa::nodeIdStringSplit(guidId, &namespaceIndex, &identifier, &identifierType));
++ QCOMPARE(namespaceIndex, 0);
++ QCOMPARE(identifierType, 'g');
++ QCOMPARE(identifier, u"72962b91-fa75-4ae6-8d28-b404dc7daf63"_s);
++ }
++ {
++ quint16 namespaceIndex = 1;
++ char identifierType = 0;
++ QString identifier;
++ QVERIFY(QOpcUa::nodeIdStringSplit(opaqueId, &namespaceIndex, &identifier, &identifierType));
++ QCOMPARE(namespaceIndex, 0);
++ QCOMPARE(identifierType, 'b');
++ QCOMPARE(identifier, u"UXQgZnR3IQ=="_s);
++ }
++
++ #ifndef QT_OPCUA_NO_NS0IDNAMES
++ // This ID is a duplicate since the last generator run
++ const auto enumEntry = QOpcUa::namespace0IdFromNodeId(u"ns=0;i=25584"_s);
++ QCOMPARE(enumEntry, QOpcUa::NodeIds::Namespace0::WellKnownRole_SecurityKeyServerPush);
++ const auto nameStr = QOpcUa::namespace0IdName(QOpcUa::NodeIds::Namespace0::WellKnownRole_SecurityKeyServerPush);
++ QCOMPARE(nameStr, u"WellKnownRole_SecurityKeyServer"_s);
++ #endif
++ }
++
++ void Tst_QOpcUaClient::getNodeForIdTypes()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=1;i=42"_s));
++ QVERIFY(node == nullptr);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ node.reset(opcuaClient->node(u"ns=1;i=42"_s));
++ QVERIFY(node != nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;i="_s));
++ QVERIFY(node == nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;s=TestString"_s));
++ QVERIFY(node != nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;s="_s));
++ QVERIFY(node == nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;g=4365723d-39fb-4139-bbea-bbfe8202b0f9"_s));
++ QVERIFY(node != nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;g=xxxxxxxxx"_s));
++ QVERIFY(node == nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;b=VGVzdElkZW50aWZpZXI="_s));
++ QVERIFY(node != nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;b="_s));
++ QVERIFY(node == nullptr);
++
++ node.reset(opcuaClient->node(u"ns=1;q=42"_s));
++ QVERIFY(node == nullptr);
++
++ node.reset(opcuaClient->node(u"xxxxxxxxxxxxx"_s));
++ QVERIFY(node == nullptr);
++ }
++
++ void Tst_QOpcUaClient::readNodeWithByteStringIdWithNullByte()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=1;b=AAABAAIADoo="_s));
++ QVERIFY(node);
++
++ READ_MANDATORY_VARIABLE_NODE(node);
++ }
++
++ void Tst_QOpcUaClient::readNS0OmitNode()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"i=84"_s)); // Root node
++ QVERIFY(node != nullptr);
++
++ READ_MANDATORY_BASE_NODE(node);
++
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(),
++ QOpcUaQualifiedName(0, u"Root"_s));
++ }
++
++ void Tst_QOpcUaClient::readInvalidNode()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=0;s=doesnotexist"_s));
++ QVERIFY(node != nullptr);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>().text(), QString());
++
++ QSignalSpy attributeReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUaNode::mandatoryBaseAttributes());
++ attributeReadSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(attributeReadSpy.size(), 1);
++ QCOMPARE(attributeReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>(), QOpcUaNode::mandatoryBaseAttributes());
++
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::DisplayName), QVariant());
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ QCOMPARE(QOpcUa::errorCategory(node->attributeError(QOpcUa::NodeAttribute::DisplayName)), QOpcUa::ErrorCategory::NodeError);
++ QVERIFY(QOpcUa::isSuccessStatus(node->attributeError(QOpcUa::NodeAttribute::DisplayName)) == false);
++ }
++
++ void Tst_QOpcUaClient::requestNotInCache()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder)));
++ QVERIFY(node != nullptr);
++
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::BadNoEntryExists);
++ QCOMPARE(node->valueAttributeError(), QOpcUa::UaStatusCode::BadNoEntryExists);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::BadNoEntryExists);
++ }
++
++ void Tst_QOpcUaClient::writeInvalidNode()
++ {
++ QFETCH(QOpcUaClient*, opcuaClient);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=0;s=doesnotexist"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy responseSpy(node.data(),&QOpcUaNode::attributeWritten);
++ bool result = node->writeAttribute(QOpcUa::NodeAttribute::Value, 10, QOpcUa::Types::Int32);
++
++ QCOMPARE(result, true);
++
++ responseSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(responseSpy.size(), 1);
++ QCOMPARE(responseSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(responseSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::writeMultipleAttributes()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.QualifiedName"_s));
++ QVERIFY(node != nullptr);
++
++ QOpcUaNode::AttributeMap map;
++ map[QOpcUa::NodeAttribute::DisplayName] = QOpcUaLocalizedText(u"en"_s, u"NewDisplayName"_s);
++ map[QOpcUa::NodeAttribute::Value] = QOpcUaQualifiedName(2, u"TestString"_s);
++
++ QSignalSpy writeSpy(node.data(), &QOpcUaNode::attributeWritten);
++
++ node->writeAttributes(map, QOpcUa::Types::QualifiedName);
++
++ writeSpy.wait(signalSpyTimeout);
++ if (writeSpy.size() < 2)
++ writeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(writeSpy.size(), 2);
++ QCOMPARE(writeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(writeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadUserAccessDenied);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::BadUserAccessDenied);
++ QCOMPARE(QOpcUa::errorCategory(node->attributeError(QOpcUa::NodeAttribute::DisplayName)), QOpcUa::ErrorCategory::PermissionError);
++
++ QCOMPARE(writeSpy.at(1).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(writeSpy.at(1).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::Value), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(2, u"TestString"_s));
++
++ writeSpy.clear();
++ node->writeAttributes({});
++
++ writeSpy.wait(signalSpyTimeout);
++ QCOMPARE(writeSpy.size(), 1);
++ QCOMPARE(writeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadNothingToDo);
++
++ node.reset(opcuaClient->node(u"ns=42;s=idonotexist"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy writeSpy2(node.data(), &QOpcUaNode::attributeWritten);
++ node->writeAttributes(map, QOpcUa::Types::QualifiedName);
++ writeSpy2.wait(signalSpyTimeout);
++ if (writeSpy2.size() < 2)
++ writeSpy2.wait(signalSpyTimeout);
++ QCOMPARE(writeSpy2.size(), 2);
++ QCOMPARE(writeSpy2.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ QCOMPARE(writeSpy2.at(1).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::readEmptyArrayVariable()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // open62541 indicates an empty array with an array length of 0 and a data pointer with value 0x1.
++ // This test makes sure that empty arrays are handled correctly without causing a segfault.
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=EmptyBoolArray"_s));
++ QVERIFY(node != nullptr);
++
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).metaType().id(), QMetaType::QVariantList);
++ QVERIFY(node->attribute(QOpcUa::NodeAttribute::Value).toList().isEmpty());
++ }
++
++ void Tst_QOpcUaClient::writeReadNodeAttributes()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QList<QOpcUaWriteItem> writeRequest;
++
++ writeRequest.append(QOpcUaWriteItem(u"ns=2;s=Demo.Static.Scalar.Double"_s, QOpcUa::NodeAttribute::Value,
++ 23.0, QOpcUa::Types::Double));
++ writeRequest.front().setSourceTimestamp(QDateTime::fromString(u"2018-08-03 01:00:00"_s, Qt::ISODate));
++ writeRequest.front().setServerTimestamp(QDateTime::fromString(u"2018-08-03T01:01:00"_s, Qt::ISODate));
++ writeRequest.append(QOpcUaWriteItem(u"ns=2;s=Demo.Static.Arrays.UInt32"_s, QOpcUa::NodeAttribute::Value,
++ QVariantList({0, 1, 2}), QOpcUa::Types::UInt32, u"0:2"_s));
++ writeRequest.append(QOpcUaWriteItem(u"ns=2;s=Demo.Static.Arrays.UInt32"_s, QOpcUa::NodeAttribute::Value,
++ QVariantList({0, 1, 2}), QOpcUa::Types::UInt32, u"0:2"_s));
++
++ // Trigger a write error by trying to update an index range with a status code mismatch.
++ writeRequest.back().setStatusCode(QOpcUa::UaStatusCode::BadDependentValueChanged);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt32"_s));
++ QVERIFY (node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariantList({1, 2, 3, 4, 5}), QOpcUa::Types::UInt32);
++
++ QSignalSpy writeNodeAttributesSpy(opcuaClient, &QOpcUaClient::writeNodeAttributesFinished);
++
++ opcuaClient->writeNodeAttributes(writeRequest);
++
++ writeNodeAttributesSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(writeNodeAttributesSpy.size(), 1);
++
++ QCOMPARE(writeNodeAttributesSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto writeResult = writeNodeAttributesSpy.at(0).at(0).value<QList<QOpcUaWriteResult>>();
++
++ QCOMPARE(writeResult.size(), 3);
++
++ for (int i = 0; i < writeResult.size(); ++i) {
++ if (i == writeResult.size() - 1)
++ QCOMPARE(writeResult[i].statusCode(), QOpcUa::UaStatusCode::BadIndexRangeInvalid); // Status code mismatch
++ else
++ QCOMPARE(writeResult[i].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(writeResult[i].nodeId(), writeRequest[i].nodeId());
++ QCOMPARE(writeResult[i].attribute(), writeRequest[i].attribute());
++ QCOMPARE(writeResult[i].indexRange(), writeRequest[i].indexRange());
++ }
++
++ // Writing finished, attempt to read back
++
++ QList<QOpcUaReadItem> readRequest;
++
++ QVERIFY (node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariantList({0, 1, 2, 3, 4}), QOpcUa::Types::UInt32);
++
++ readRequest.push_back(QOpcUaReadItem(u"ns=2;s=Demo.Static.Scalar.Double"_s,
++ QOpcUa::NodeAttribute::DisplayName));
++ readRequest.push_back(QOpcUaReadItem(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ readRequest.push_back(QOpcUaReadItem(u"ns=2;s=Demo.Static.Arrays.UInt32"_s,
++ QOpcUa::NodeAttribute::Value, u"0:2"_s));
++
++ QSignalSpy readNodeAttributesSpy(opcuaClient, &QOpcUaClient::readNodeAttributesFinished);
++
++ opcuaClient->readNodeAttributes(readRequest);
++
++ readNodeAttributesSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readNodeAttributesSpy.size(), 1);
++
++ QCOMPARE(readNodeAttributesSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto readResult = readNodeAttributesSpy.at(0).at(0).value<QList<QOpcUaReadResult>>();
++
++ QCOMPARE(readResult.size(), 3);
++
++ for (int i = 0; i < readResult.size(); ++i) {
++ QCOMPARE(readResult[i].statusCode(), QOpcUa::UaStatusCode::Good);
++ QVERIFY(readResult[i].serverTimestamp().isValid());
++ QCOMPARE(readResult[i].nodeId(), readRequest[i].nodeId());
++ QCOMPARE(readResult[i].attribute(), readRequest[i].attribute());
++ QCOMPARE(readResult[i].indexRange(), readRequest[i].indexRange());
++ }
++
++ QVERIFY(!readResult[0].sourceTimestamp().isValid()); // The initial DisplayName attribute doesn't have a source timestamp
++ QVERIFY(readResult[1].sourceTimestamp().isValid());
++ QVERIFY(readResult[2].sourceTimestamp().isValid());
++ QCOMPARE(readResult[0].value().value<QOpcUaLocalizedText>().text(), u"DoubleScalarTest"_s);
++ QCOMPARE(readResult[1].value(), 23.0);
++ QCOMPARE(readResult[2].value(), QVariantList({0, 1, 2}));
++ // Only check the source timestamp, the server timestamp is replaced with the current DateTime in the open62541
++ // server's Read service.
++ QCOMPARE(readResult[1].sourceTimestamp(), QDateTime::fromString(u"2018-08-03 01:00:00"_s, Qt::ISODate));
++ }
++
++ void Tst_QOpcUaClient::readDataTypeDefinition()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Range)));
++ QVERIFY (node != nullptr);
++
++ node->readAttributes(QOpcUa::NodeAttribute::DataTypeDefinition);
++
++ QSignalSpy spy(node.get(), &QOpcUaNode::attributeRead);
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).value<QOpcUa::NodeAttributes>().testFlag(QOpcUa::NodeAttribute::DataTypeDefinition), true);
++
++ qDebug() << node->valueAttribute();
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::DataTypeDefinition).canConvert<QOpcUaStructureDefinition>(), true);
++ }
++
++ void Tst_QOpcUaClient::readAccessLevelEx()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt32"_s));
++ QVERIFY (node != nullptr);
++
++ QSignalSpy spy(node.get(), &QOpcUaNode::attributeRead);
++ node->readAttributes(QOpcUa::NodeAttribute::AccessLevelEx);
++
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).value<QOpcUa::NodeAttributes>().testFlag(QOpcUa::NodeAttribute::AccessLevelEx), true);
++
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::AccessLevelEx), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::AccessLevelEx).value<quint32>(), 99);
++ QOpcUa::AccessLevelEx ex(node->attribute(QOpcUa::NodeAttribute::AccessLevelEx).value<quint32>());
++ QVERIFY(ex.testFlag(QOpcUa::AccessLevelExBit::CurrentRead));
++ QVERIFY(ex.testFlag(QOpcUa::AccessLevelExBit::CurrentWrite));
++ }
++
++ void Tst_QOpcUaClient::readNewPermissionAttributes()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt32"_s));
++ QVERIFY (node != nullptr);
++
++ QSignalSpy spy(node.get(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::RolePermissions | QOpcUa::NodeAttribute::UserRolePermissions
++ | QOpcUa::NodeAttribute::AccessRestrictions);
++
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).value<QOpcUa::NodeAttributes>().testFlag(QOpcUa::NodeAttribute::RolePermissions), true);
++ QCOMPARE(spy.at(0).at(0).value<QOpcUa::NodeAttributes>().testFlag(QOpcUa::NodeAttribute::UserRolePermissions), true);
++ QCOMPARE(spy.at(0).at(0).value<QOpcUa::NodeAttributes>().testFlag(QOpcUa::NodeAttribute::AccessRestrictions), true);
++
++ // The open62541 server doesn't yet support the three attributes, so we can only check if a read was attempted
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::RolePermissions), QOpcUa::UaStatusCode::BadAttributeIdInvalid);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::UserRolePermissions), QOpcUa::UaStatusCode::BadAttributeIdInvalid);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::AccessRestrictions), QOpcUa::UaStatusCode::BadAttributeIdInvalid);
++ }
++
++ void Tst_QOpcUaClient::getRootNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> root(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder)));
++ QVERIFY(root != nullptr);
++ QVERIFY(root->client() == opcuaClient);
++
++ READ_MANDATORY_BASE_NODE(root)
++ QCOMPARE(root->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>().text(), u"Root"_s);
++
++ QString nodeId = root->nodeId();
++ QCOMPARE(nodeId, QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ }
++
++ void Tst_QOpcUaClient::getChildren()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=1;s=Large.Folder"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++ node->browseChildren(QOpcUa::ReferenceTypeId::HierarchicalReferences, QOpcUa::NodeClass::Object);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 100);
++ }
++
++ void Tst_QOpcUaClient::childrenIdsString()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=testStringIdsFolder"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes, QOpcUa::NodeClass::Variable);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 1);
++ QCOMPARE(ref.at(0).targetNodeId().nodeId(), u"ns=3;s=theStringId"_s);
++ }
++
++ void Tst_QOpcUaClient::childrenIdsGuidNodeId()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=testGuidIdsFolder"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes, QOpcUa::NodeClass::Variable);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 1);
++ QCOMPARE(ref.at(0).targetNodeId().nodeId(), u"ns=3;g=08081e75-8e5e-319b-954f-f3a7613dc29b"_s);
++ }
++
++ void Tst_QOpcUaClient::childrenIdsOpaqueNodeId()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=testOpaqueIdsFolder"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes, QOpcUa::NodeClass::Variable);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 1);
++ QCOMPARE(ref.at(0).targetNodeId().nodeId(), u"ns=3;b=UXQgZnR3IQ=="_s);
++ }
++
++ void Tst_QOpcUaClient::testSpecialCharStringNodeIds()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // Get child node id
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=ümläutFölderNödeId"_s));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes, QOpcUa::NodeClass::Variable);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ QList<QOpcUaReferenceDescription> ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 1);
++ QCOMPARE(ref.at(0).targetNodeId().nodeId(), u"ns=3;s=ümläutVäriableNödeId"_s);
++ QCOMPARE(ref.at(0).browseName().namespaceIndex(), 3);
++ QCOMPARE(ref.at(0).browseName().name(), u"ümläutVäriableNödeId"_s);
++
++ // Make sure the id can be used for subsequent requests
++
++ node.reset(opcuaClient->node(ref.at(0).targetNodeId().nodeId()));
++ QSignalSpy readSpy(node.data(), &QOpcUaNode::attributeUpdated);
++
++ node->readAttributes(QOpcUa::NodeAttribute::BrowseName);
++ readSpy.wait(signalSpyTimeout);
++ QCOMPARE(readSpy.size(), 1);
++
++ QOpcUaQualifiedName nameFromSignal = readSpy.at(0).at(1).value<QOpcUaQualifiedName>();
++ QOpcUaQualifiedName nameFromAttributes = node->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>();
++
++ QCOMPARE(nameFromSignal.namespaceIndex(), 3);
++ QCOMPARE(nameFromSignal.name(), u"ümläutVäriableNödeId"_s);
++
++ QCOMPARE(nameFromAttributes.namespaceIndex(), 3);
++ QCOMPARE(nameFromAttributes.name(), u"ümläutVäriableNödeId"_s);
++ }
++
++ void Tst_QOpcUaClient::inverseBrowse()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Boolean)));
++ QVERIFY(node != nullptr);
++ QSignalSpy spy(node.data(), &QOpcUaNode::browseFinished);
++
++ QOpcUaBrowseRequest request;
++ request.setReferenceTypeId(QOpcUa::ReferenceTypeId::References);
++ request.setIncludeSubtypes(true);
++ request.setNodeClassMask(QOpcUa::NodeClass::Undefined);
++ request.setBrowseDirection(QOpcUaBrowseRequest::BrowseDirection::Inverse);
++ node->browse(request);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto ref = spy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QCOMPARE(ref.size(), 1);
++ QCOMPARE(ref.at(0).targetNodeId().nodeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseDataType));
++ QCOMPARE(ref.at(0).isForwardReference(), false);
++ QCOMPARE(ref.at(0).browseName().name(), u"BaseDataType"_s);
++ QCOMPARE(ref.at(0).displayName().text(), u"BaseDataType"_s);
++ QCOMPARE(ref.at(0).refTypeId(), QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasSubtype));
++ QCOMPARE(ref.at(0).nodeClass(), QOpcUa::NodeClass::DataType);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveObjectNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(u"ns=3;s=TestFolder"_s);
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicObjectNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicObjectNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setEventNotifier(QOpcUa::EventNotifierBit::SubscribeToEvents);
++ attributes.setWriteMask(QOpcUa::WriteMaskBit::DisplayName);
++ attributes.setUserWriteMask(QOpcUa::WriteMaskBit::DisplayName);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::Organizes));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::Object);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::EventNotifier |
++ QOpcUa::NodeAttribute::WriteMask | QOpcUa::NodeAttribute::UserWriteMask);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::EventNotifier), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::WriteMask), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::UserWriteMask), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::EventNotifier).value<quint8>(),
++ static_cast<quint8>(QOpcUa::EventNotifierBit::SubscribeToEvents));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::WriteMask).value<quint8>(),
++ static_cast<quint8>(QOpcUa::WriteMaskBit::DisplayName));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::UserWriteMask).value<quint8>(),
++ static_cast<quint8>(QOpcUa::WriteMaskBit::DisplayName));
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveVariableNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(u"ns=3;s=TestFolder"_s);
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicVariableNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicVariableNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setValue(QOpcUaMultiDimensionalArray({1,2,3,4}, {2,2}), QOpcUa::Types::Double);
++ attributes.setDataTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Double));
++ attributes.setValueRank(2); // Two-dimensional array
++ attributes.setArrayDimensions({ 2, 2 });
++ attributes.setMinimumSamplingInterval(23.0);
++ attributes.setHistorizing(true);
++
++ attributes.setAccessLevel(QOpcUa::AccessLevelBit::CurrentRead);
++ attributes.setUserAccessLevel(QOpcUa::AccessLevelBit::CurrentRead);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::Organizes));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::Variable);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::Value |
++ QOpcUa::NodeAttribute::ValueRank | QOpcUa::NodeAttribute::ArrayDimensions |
++ QOpcUa::NodeAttribute::MinimumSamplingInterval | QOpcUa::NodeAttribute::Historizing);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Value), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::ValueRank), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::ArrayDimensions), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::MinimumSamplingInterval), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Historizing), QOpcUa::UaStatusCode::Good);
++
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaMultiDimensionalArray>().valueArray(), QVariantList({1, 2, 3, 4}));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::ValueRank), 2);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::ArrayDimensions).toList(), QVariantList({ 2, 2 }));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::MinimumSamplingInterval).toDouble(), 23.0);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Historizing), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveMethodNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(u"ns=3;s=TestFolder"_s);
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicMethodNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicMethodNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setExecutable(true);
++ attributes.setUserExecutable(true);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::Organizes));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::Method);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::Executable |
++ QOpcUa::NodeAttribute::UserExecutable);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Executable), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::UserExecutable), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Executable), true);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::UserExecutable), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveObjectTypeNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseObjectType));
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicObjectTypeNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicObjectTypeNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setIsAbstract(true);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasSubtype));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::ObjectType);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::IsAbstract);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::IsAbstract), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::IsAbstract), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveVariableTypeNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseVariableType));
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicVariableTypeNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicVariableTypeNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setValue(QOpcUaMultiDimensionalArray({1,2,3,4}, {2,2}), QOpcUa::Types::Double);
++ attributes.setDataTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Double));
++ attributes.setValueRank(2); // Two-dimensional array
++ attributes.setArrayDimensions({ 2, 2 });
++ attributes.setIsAbstract(true);
++
++ attributes.setAccessLevel(QOpcUa::AccessLevelBit::CurrentRead);
++ attributes.setUserAccessLevel(QOpcUa::AccessLevelBit::CurrentRead);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasSubtype));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::VariableType);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::Value |
++ QOpcUa::NodeAttribute::ValueRank | QOpcUa::NodeAttribute::ArrayDimensions |
++ QOpcUa::NodeAttribute::IsAbstract);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Value), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::ValueRank), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::ArrayDimensions), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::IsAbstract), QOpcUa::UaStatusCode::Good);
++
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaMultiDimensionalArray>().valueArray(), QVariantList({1, 2, 3, 4}));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::ValueRank), 2);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::ArrayDimensions).toList(), QVariantList({ 2, 2 }));
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::IsAbstract), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveReferenceTypeNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Organizes));
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicReferenceTypeNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicReferenceTypeNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setIsAbstract(true);
++ attributes.setSymmetric(false);
++ attributes.setInverseName({ u"en"_s, u"Inverse name"_s });
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasSubtype));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::ReferenceType);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::IsAbstract |
++ QOpcUa::NodeAttribute::Symmetric | QOpcUa::NodeAttribute::InverseName);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::IsAbstract), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Symmetric), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::InverseName), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::IsAbstract), true);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Symmetric), false);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::InverseName), QOpcUaLocalizedText(u"en"_s, u"Inverse name"_s));
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveDataTypeNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseDataType));
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicDataTypeNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicDataTypeNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setIsAbstract(true);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasSubtype));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::DataType);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::IsAbstract);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::IsAbstract), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::IsAbstract), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveViewNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addNodeSpy(opcuaClient, &QOpcUaClient::addNodeFinished);
++
++ QOpcUaExpandedNodeId parent;
++ parent.setNodeId(u"ns=3;s=TestFolder"_s);
++ QOpcUaExpandedNodeId requestedNewId;
++ requestedNewId.setNodeId(u"ns=3;s=DynamicViewNode_%1"_s.arg(opcuaClient->backend()));
++ const quint16 namespaceIndex = 3;
++ QOpcUaQualifiedName browseName(namespaceIndex, u"DynamicViewNode_%1"_s.arg(opcuaClient->backend()));
++
++ QOpcUaNodeCreationAttributes attributes;
++
++ QOpcUaLocalizedText displayName(u"en"_s, browseName.name());
++ QOpcUaLocalizedText description(u"en"_s, u"Node added at runtime by %1"_s.arg(opcuaClient->backend()));
++
++ attributes.setDisplayName(displayName);
++ attributes.setDescription(description);
++ attributes.setContainsNoLoops(true);
++ attributes.setEventNotifier(QOpcUa::EventNotifierBit::SubscribeToEvents);
++
++ QOpcUaAddNodeItem nodeInfo;
++ nodeInfo.setParentNodeId(parent);
++ nodeInfo.setReferenceTypeId(QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::Organizes));
++ nodeInfo.setRequestedNewNodeId(requestedNewId);
++ nodeInfo.setBrowseName(browseName);
++ nodeInfo.setNodeClass(QOpcUa::NodeClass::View);
++ nodeInfo.setNodeAttributes(attributes);
++
++ opcuaClient->addNode(nodeInfo);
++
++ addNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addNodeSpy.size(), 1);
++
++ QCOMPARE(addNodeSpy.at(0).at(0).value<QOpcUaExpandedNodeId>().nodeId(), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(1), requestedNewId.nodeId());
++ QCOMPARE(addNodeSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> newNode(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(newNode != nullptr);
++
++ QSignalSpy readSpy(newNode.data(), &QOpcUaNode::attributeRead);
++ newNode->readAttributes(QOpcUa::NodeAttribute::BrowseName | QOpcUa::NodeAttribute::DisplayName |
++ QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::ContainsNoLoops |
++ QOpcUa::NodeAttribute::EventNotifier);
++ readSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readSpy.size(), 1);
++
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::BrowseName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::DisplayName), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::Description), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::ContainsNoLoops), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(newNode->attributeError(QOpcUa::NodeAttribute::EventNotifier), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), browseName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(), displayName);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(), description);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::ContainsNoLoops), true);
++ QCOMPARE(newNode->attribute(QOpcUa::NodeAttribute::EventNotifier), true);
++
++ QSignalSpy removeNodeSpy(opcuaClient, &QOpcUaClient::deleteNodeFinished);
++ opcuaClient->deleteNode(requestedNewId.nodeId(), true);
++ removeNodeSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(removeNodeSpy.size(), 1);
++ QCOMPARE(removeNodeSpy.at(0).at(0), requestedNewId.nodeId());
++ QCOMPARE(removeNodeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(requestedNewId.nodeId()));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy unsuccessfulReadSpy(node.data(), &QOpcUaNode::attributeRead);
++
++ node->readAttributes(QOpcUa::NodeAttribute::NodeId);
++ unsuccessfulReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(unsuccessfulReadSpy.size(), 1);
++ QVERIFY(unsuccessfulReadSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::NodeId), QOpcUa::UaStatusCode::BadNodeIdUnknown);
++ }
++
++ void Tst_QOpcUaClient::addAndRemoveReference()
++ {
++
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy addReferenceSpy(opcuaClient, &QOpcUaClient::addReferenceFinished);
++
++ QOpcUaExpandedNodeId target;
++ target.setNodeId(u"ns=3;s=TestFolder"_s);
++ QString referenceType = QOpcUa::nodeIdFromInteger(0, static_cast<quint32>(QOpcUa::ReferenceTypeId::Organizes));
++
++ QOpcUaAddReferenceItem refInfo;
++ refInfo.setSourceNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ refInfo.setReferenceTypeId(referenceType);
++ refInfo.setIsForwardReference(true);
++ refInfo.setTargetNodeId(target);
++ refInfo.setTargetNodeClass(QOpcUa::NodeClass::Object);
++
++ opcuaClient->addReference(refInfo);
++ addReferenceSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(addReferenceSpy.size(), 1);
++ QCOMPARE(addReferenceSpy.at(0).at(0), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ QCOMPARE(addReferenceSpy.at(0).at(1), referenceType);
++ QCOMPARE(addReferenceSpy.at(0).at(2).value<QOpcUaExpandedNodeId>().nodeId(), target.nodeId());
++ QCOMPARE(addReferenceSpy.at(0).at(3).value<bool>(), true);
++ QCOMPARE(addReferenceSpy.at(0).at(4).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ // Check if the reference has been added
++ {
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder)));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy browseSpy(node.data(), &QOpcUaNode::browseFinished);
++
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes);
++ browseSpy.wait(signalSpyTimeout);
++ QCOMPARE(browseSpy.size(), 1);
++ QCOMPARE(browseSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto results = browseSpy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QVERIFY(!results.isEmpty());
++ bool referenceExists = false;
++ for (const auto &item : results) {
++ if (item.targetNodeId().nodeId() == target.nodeId()) {
++ referenceExists = true;
++ break;
++ }
++ }
++ QVERIFY(referenceExists);
++ }
++
++ QSignalSpy deleteReferenceSpy(opcuaClient, &QOpcUaClient::deleteReferenceFinished);
++
++ QOpcUaDeleteReferenceItem refDelInfo;
++ refDelInfo.setSourceNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ refDelInfo.setReferenceTypeId(referenceType);
++ refDelInfo.setIsForwardReference(true);
++ refDelInfo.setTargetNodeId(target);
++ refDelInfo.setDeleteBidirectional(true);
++
++ opcuaClient->deleteReference(refDelInfo);
++ deleteReferenceSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(deleteReferenceSpy.size(), 1);
++ QCOMPARE(deleteReferenceSpy.at(0).at(0), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ QCOMPARE(deleteReferenceSpy.at(0).at(1), referenceType);
++ QCOMPARE(deleteReferenceSpy.at(0).at(2).value<QOpcUaExpandedNodeId>().nodeId(), target.nodeId());
++ QCOMPARE(deleteReferenceSpy.at(0).at(3).value<bool>(), true);
++ QCOMPARE(deleteReferenceSpy.at(0).at(4).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ // Check if the reference has been deleted
++ {
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder)));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy browseSpy(node.data(), &QOpcUaNode::browseFinished);
++
++ node->browseChildren(QOpcUa::ReferenceTypeId::Organizes);
++ browseSpy.wait(signalSpyTimeout);
++ QCOMPARE(browseSpy.size(), 1);
++ QCOMPARE(browseSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto results = browseSpy.at(0).at(0).value<QList<QOpcUaReferenceDescription>>();
++ QVERIFY(!results.isEmpty());
++ for (const auto &item : results)
++ QVERIFY(item.targetNodeId().nodeId() != target.nodeId());
++ }
++
++ // Check error paths
++ addReferenceSpy.clear();
++ refInfo.setTargetNodeId(QOpcUaExpandedNodeId(u"ns=42;i=1234"_s));
++ opcuaClient->addReference(refInfo);
++ addReferenceSpy.wait(signalSpyTimeout);
++ QCOMPARE(addReferenceSpy.size(), 1);
++ QCOMPARE(addReferenceSpy.at(0).at(4), QOpcUa::UaStatusCode::BadTargetNodeIdInvalid);
++
++ deleteReferenceSpy.clear();
++ refDelInfo.setTargetNodeId(QOpcUaExpandedNodeId(u"ns=42;i=1234"_s));
++ opcuaClient->deleteReference(refDelInfo);
++ deleteReferenceSpy.wait(signalSpyTimeout);
++ QCOMPARE(deleteReferenceSpy.size(), 1);
++ QCOMPARE(addReferenceSpy.at(0).at(4), QOpcUa::UaStatusCode::BadTargetNodeIdInvalid);
++ }
++
++ void Tst_QOpcUaClient::dataChangeSubscription()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(readWriteNode));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant(double(0)), QOpcUa::Types::Double);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).toDouble(), 0.0);
++
++ QSignalSpy dataChangeSpy(node.data(), &QOpcUaNode::dataChangeOccurred);
++ QSignalSpy monitoringEnabledSpy(node.data(), &QOpcUaNode::enableMonitoringFinished);
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100, QOpcUaMonitoringParameters::SubscriptionType::Exclusive));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QOpcUaMonitoringParameters valueStatus = node->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QVERIFY(valueStatus.subscriptionId() != 0);
++ QVERIFY(valueStatus.monitoredItemId() != 0);
++ QCOMPARE(valueStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ WRITE_VALUE_ATTRIBUTE(node, QVariant(double(42)), QOpcUa::Types::Double);
++ dataChangeSpy.wait(signalSpyTimeout);
++ if (dataChangeSpy.empty() || !qFuzzyCompare(dataChangeSpy.at(0).at(1).toDouble(), 42.0))
++ dataChangeSpy.wait(signalSpyTimeout);
++
++ QVERIFY(dataChangeSpy.size() >= 1);
++
++ int index = dataChangeSpy.size() == 1 ? 0 : 1;
++ QCOMPARE(dataChangeSpy.at(index).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(dataChangeSpy.at(index).at(1), double(42));
++
++ monitoringEnabledSpy.clear();
++ dataChangeSpy.clear();
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::DisplayName, QOpcUaMonitoringParameters(100,QOpcUaMonitoringParameters::SubscriptionType::Exclusive,
++ valueStatus.subscriptionId()));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::DisplayName).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QOpcUaMonitoringParameters displayNameStatus = node->monitoringStatus(QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(displayNameStatus.subscriptionId(), valueStatus.subscriptionId());
++ QVERIFY(displayNameStatus.monitoredItemId() != 0);
++ QCOMPARE(displayNameStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(dataChangeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(dataChangeSpy.at(0).at(1).value<QOpcUaLocalizedText>().text(), u"TestNode.ReadWrite"_s);
++
++ monitoringEnabledSpy.clear();
++ dataChangeSpy.clear();
++ node->enableMonitoring(QOpcUa::NodeAttribute::NodeId, QOpcUaMonitoringParameters(100));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::NodeId);
++ QVERIFY(node->monitoringStatus(QOpcUa::NodeAttribute::NodeId).subscriptionId() != valueStatus.subscriptionId());
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::NodeId).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QOpcUaMonitoringParameters nodeIdStatus = node->monitoringStatus(QOpcUa::NodeAttribute::NodeId);
++ QVERIFY(nodeIdStatus.subscriptionId() != valueStatus.subscriptionId());
++ QVERIFY(nodeIdStatus.monitoredItemId() != 0);
++ QCOMPARE(nodeIdStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(dataChangeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::NodeId);
++ QCOMPARE(dataChangeSpy.at(0).at(1), u"ns=3;s=TestNode.ReadWrite"_s);
++
++ QList<QOpcUa::NodeAttribute> attrs;
++
++ QSignalSpy monitoringModifiedSpy(node.data(), &QOpcUaNode::monitoringStatusChanged);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::PublishingInterval, 200);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::LifetimeCount, valueStatus.lifetimeCount() + 1);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount, valueStatus.maxKeepAliveCount() + 1);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::Priority, valueStatus.priority() + 1);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish,
++ valueStatus.maxNotificationsPerPublish());
++
++ for (int i = 0; i < 10; ++i) {
++ monitoringModifiedSpy.wait(signalSpyTimeout);
++
++ if (monitoringModifiedSpy.size() == 10)
++ break;
++ }
++
++ QCOMPARE(monitoringModifiedSpy.size(), 10);
++
++ const QOpcUaMonitoringParameters::Parameters params =
++ QOpcUaMonitoringParameters::Parameter::PublishingInterval |
++ QOpcUaMonitoringParameters::Parameter::LifetimeCount |
++ QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount |
++ QOpcUaMonitoringParameters::Parameter::Priority |
++ QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish;
++
++ QHash<QOpcUa::NodeAttribute, QOpcUaMonitoringParameters::Parameters> expectedEntries = {
++ { QOpcUa::NodeAttribute::Value, params },
++ { QOpcUa::NodeAttribute::DisplayName, params }
++ };
++
++ for (auto &it : std::as_const(monitoringModifiedSpy)) {
++ QOpcUa::NodeAttribute temp = it.at(0).value<QOpcUa::NodeAttribute>();
++ QVERIFY(expectedEntries.contains(temp));
++ const auto includedFlags = it.at(1).value<QOpcUaMonitoringParameters::Parameters>();
++ QCOMPARE(includedFlags.testAnyFlags(expectedEntries.value(temp)), true);
++ QCOMPARE(it.at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ expectedEntries[temp].setFlag(QOpcUaMonitoringParameters::Parameter(includedFlags.toInt()), false);
++ }
++
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::Value).publishingInterval(), 200.0);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::DisplayName).publishingInterval(), 200.0);
++ QCOMPARE(expectedEntries.value(QOpcUa::NodeAttribute::Value).toInt(), 0);
++ QCOMPARE(expectedEntries.value(QOpcUa::NodeAttribute::DisplayName).toInt(), 0);
++
++ QSignalSpy monitoringDisabledSpy(node.data(), &QOpcUaNode::disableMonitoringFinished);
++
++ node->disableMonitoring(QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::DisplayName | QOpcUa::NodeAttribute::NodeId);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ if (monitoringDisabledSpy.size() < 2)
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ if (monitoringDisabledSpy.size() < 3)
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringDisabledSpy.size(), 3);
++
++ attrs = {QOpcUa::NodeAttribute::Value, QOpcUa::NodeAttribute::DisplayName, QOpcUa::NodeAttribute::NodeId};
++ for (const auto &it : std::as_const(monitoringDisabledSpy)) {
++ QOpcUa::NodeAttribute temp = it.at(0).value<QOpcUa::NodeAttribute>();
++ QVERIFY(attrs.contains(temp));
++ QCOMPARE(node->monitoringStatus(temp).subscriptionId(), (quint32)0);
++ QCOMPARE(node->monitoringStatus(temp).monitoredItemId(), (quint32)0);
++ QCOMPARE(node->monitoringStatus(temp).statusCode(), QOpcUa::UaStatusCode::BadNoEntryExists);
++ attrs.remove(attrs.indexOf(temp));
++ }
++ QCOMPARE(attrs.size(), 0);
++
++ // Enable the monitoring again, after it has been disabled. This triggered a bug where a previous subscription
++ // was not completeley removed from the internal data structures
++ monitoringEnabledSpy.clear();
++ node->enableMonitoring(QOpcUa::NodeAttribute::NodeId, QOpcUaMonitoringParameters(100));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::NodeId);
++ QVERIFY(node->monitoringStatus(QOpcUa::NodeAttribute::NodeId).subscriptionId() != valueStatus.subscriptionId());
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::NodeId).statusCode(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::dataChangeSubscriptionInvalidNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> noDataNode(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder)));
++ QVERIFY(noDataNode != nullptr);
++ QSignalSpy monitoringEnabledSpy(noDataNode.data(), &QOpcUaNode::enableMonitoringFinished);
++
++ QOpcUaMonitoringParameters settings;
++ settings.setPublishingInterval(100);
++ noDataNode->enableMonitoring(QOpcUa::NodeAttribute::Value, settings);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(noDataNode->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::BadAttributeIdInvalid);
++ QCOMPARE(noDataNode->monitoringStatus(QOpcUa::NodeAttribute::Value).subscriptionId(), (quint32)0);
++ }
++
++ void Tst_QOpcUaClient::dataChangeSubscriptionSharing()
++ {
++ // The open62541 test server has a minimum publishing interval of 100ms.
++ // This test verifies that monitorings with smaller requested publishing interval and shared flag
++ // share the same subscription.
++
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(readWriteNode));
++ QVERIFY(node != nullptr);
++ QSignalSpy monitoringEnabledSpy(node.data(), &QOpcUaNode::enableMonitoringFinished);
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(50));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QOpcUaMonitoringParameters valueStatus = node->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QVERIFY(valueStatus.subscriptionId() != 0);
++ QCOMPARE(valueStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ monitoringEnabledSpy.clear();
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::DisplayName, QOpcUaMonitoringParameters(25));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(node->monitoringStatus(QOpcUa::NodeAttribute::DisplayName).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QOpcUaMonitoringParameters displayNameStatus = node->monitoringStatus(QOpcUa::NodeAttribute::DisplayName);
++ QCOMPARE(displayNameStatus.subscriptionId(), valueStatus.subscriptionId());
++ QCOMPARE(displayNameStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(valueStatus.subscriptionId(), displayNameStatus.subscriptionId());
++ QCOMPARE(valueStatus.publishingInterval(), displayNameStatus.publishingInterval());
++ QCOMPARE(valueStatus.publishingInterval(), 100.0);
++
++ QSignalSpy monitoringDisabledSpy(node.data(), &QOpcUaNode::disableMonitoringFinished);
++
++ node->disableMonitoring(QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::DisplayName);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ if (monitoringDisabledSpy.size() < 2)
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringDisabledSpy.size(), 2);
++
++ QList<QOpcUa::NodeAttribute> attrs = {QOpcUa::NodeAttribute::Value, QOpcUa::NodeAttribute::DisplayName};
++ for (const auto &it : std::as_const(monitoringDisabledSpy)) {
++ auto temp = it.at(0).value<QOpcUa::NodeAttribute>();
++ QVERIFY(attrs.contains(temp));
++ QCOMPARE(node->monitoringStatus(temp).subscriptionId(), (quint32)0);
++ QCOMPARE(node->monitoringStatus(temp).statusCode(), QOpcUa::UaStatusCode::BadNoEntryExists);
++ attrs.removeOne(temp);
++ }
++ QCOMPARE(attrs.size(), 0);
++ }
++
++ void Tst_QOpcUaClient::dataChangeSubscriptionTriggering()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // Setup triggered node and write node
++ QScopedPointer<QOpcUaNode> writeNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(writeNode != nullptr);
++ QScopedPointer<QOpcUaNode> triggeredNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(triggeredNode != nullptr);
++
++ WRITE_VALUE_ATTRIBUTE(writeNode, 1, QOpcUa::Types::Int32);
++
++ QOpcUaMonitoringParameters triggeredParams(50);
++ triggeredParams.setMonitoringMode(QOpcUaMonitoringParameters::MonitoringMode::Sampling);
++ triggeredParams.setQueueSize(10);
++
++ triggeredNode->enableMonitoring(QOpcUa::NodeAttribute::Value, triggeredParams);
++ QSignalSpy monitoringEnabledSpy2(triggeredNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy valueChangedSpy(triggeredNode.data(), &QOpcUaNode::attributeUpdated);
++ monitoringEnabledSpy2.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy2.size(), 1);
++ QCOMPARE(monitoringEnabledSpy2.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ const auto triggeredStatus = triggeredNode->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(triggeredStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ // Setup trigger
++ QScopedPointer<QOpcUaNode> triggeringNode(opcuaClient->node(readWriteNode));
++ QVERIFY(triggeringNode != nullptr);
++ QSignalSpy monitoringEnabledSpy(triggeringNode.data(), &QOpcUaNode::enableMonitoringFinished);
++
++ QOpcUaMonitoringParameters triggerParameters(50);
++ triggerParameters.setSubscriptionId(triggeredStatus.subscriptionId());
++ triggerParameters.setTriggeredItemIds({ triggeredStatus.monitoredItemId(), 10, 11 });
++ triggeringNode->enableMonitoring(QOpcUa::NodeAttribute::Value, triggerParameters);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(triggeringNode->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ const auto triggerStatus = triggeringNode->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QVERIFY(triggerStatus.subscriptionId() != 0);
++ QCOMPARE(triggerStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(triggerStatus.subscriptionId(), triggeredStatus.subscriptionId());
++ QCOMPARE(triggerStatus.triggeredItemIds(), QSet<quint32>{ triggeredStatus.monitoredItemId() });
++ QCOMPARE(triggerStatus.failedTriggeredItemsStatus(),
++ (QHash<quint32, QOpcUa::UaStatusCode>{
++ { 10, QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid },
++ { 11, QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid }
++ }));
++
++ // Create value change, trigger and check results
++
++ WRITE_VALUE_ATTRIBUTE(writeNode, 2, QOpcUa::Types::Int32);
++
++ valueChangedSpy.wait(1000);
++ QCOMPARE(valueChangedSpy.size(), 0);
++
++ WRITE_VALUE_ATTRIBUTE(triggeringNode, 1234.0, QOpcUa::Types::Double);
++
++ valueChangedSpy.wait(signalSpyTimeout);
++ QCOMPARE(valueChangedSpy.size(), 1);
++ QCOMPARE(valueChangedSpy.at(0).at(1).value<quint32>(), 2U);
++ }
++
++ void Tst_QOpcUaClient::dataChangeSubscriptionModifyTriggering()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // Setup triggered and write nodes
++ QScopedPointer<QOpcUaNode> writeNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(writeNode != nullptr);
++ QScopedPointer<QOpcUaNode> triggeredNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(triggeredNode != nullptr);
++
++ WRITE_VALUE_ATTRIBUTE(writeNode, 1, QOpcUa::Types::Int32);
++
++ QOpcUaMonitoringParameters triggeredParams(50);
++ triggeredParams.setMonitoringMode(QOpcUaMonitoringParameters::MonitoringMode::Sampling);
++ triggeredParams.setQueueSize(10);
++
++ triggeredNode->enableMonitoring(QOpcUa::NodeAttribute::Value, triggeredParams);
++ QSignalSpy monitoringEnabledSpy2(triggeredNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy valueChangedSpy(triggeredNode.data(), &QOpcUaNode::attributeUpdated);
++ monitoringEnabledSpy2.wait();
++
++ QCOMPARE(monitoringEnabledSpy2.size(), 1);
++ QCOMPARE(monitoringEnabledSpy2.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ const auto triggeredStatus = triggeredNode->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(triggeredStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ // Setup triggering node
++ QScopedPointer<QOpcUaNode> triggeringNode(opcuaClient->node(readWriteNode));
++ QVERIFY(triggeringNode != nullptr);
++ QSignalSpy monitoringEnabledSpy(triggeringNode.data(), &QOpcUaNode::enableMonitoringFinished);
++
++ QOpcUaMonitoringParameters triggerSettings(50);
++ triggerSettings.setSubscriptionId(triggeredStatus.subscriptionId());
++ triggeringNode->enableMonitoring(QOpcUa::NodeAttribute::Value, triggerSettings);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ auto triggerStatus = triggeringNode->monitoringStatus(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(triggerStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(triggerStatus.subscriptionId(), triggeredStatus.subscriptionId());
++ QCOMPARE(triggerStatus.statusCode(), QOpcUa::UaStatusCode::Good);
++
++ // Write, nothing should happen
++ WRITE_VALUE_ATTRIBUTE(writeNode, 2, QOpcUa::Types::Int32);
++
++ valueChangedSpy.wait(1000);
++ QCOMPARE(valueChangedSpy.size(), 0);
++
++ WRITE_VALUE_ATTRIBUTE(triggeringNode, 1235.0, QOpcUa::Types::Double);
++
++ valueChangedSpy.wait(1000);
++ QCOMPARE(valueChangedSpy.size(), 0);
++
++ // Modify triggering node to actually trigger
++ QSignalSpy modifySpy(triggeringNode.get(), &QOpcUaNode::monitoringStatusChanged);
++ triggeringNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::TriggeredItemIds,
++ QVariant::fromValue(QSet<quint32>{triggeredStatus.monitoredItemId(), 10, 11}));
++ modifySpy.wait(signalSpyTimeout);
++ QCOMPARE(modifySpy.size(), 1);
++ QCOMPARE(modifySpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(triggeringNode->monitoringStatus(QOpcUa::NodeAttribute::Value).triggeredItemIds(),
++ QSet<quint32>{triggeredStatus.monitoredItemId()});
++ QCOMPARE(triggeringNode->monitoringStatus(QOpcUa::NodeAttribute::Value).failedTriggeredItemsStatus(),
++ (QHash<quint32, QOpcUa::UaStatusCode>{
++ { 10, QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid },
++ { 11, QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid }
++ }));
++
++ WRITE_VALUE_ATTRIBUTE(writeNode, 3, QOpcUa::Types::Int32);
++
++ valueChangedSpy.wait(1000);
++ QCOMPARE(valueChangedSpy.size(), 0);
++
++ WRITE_VALUE_ATTRIBUTE(triggeringNode, 1236.0, QOpcUa::Types::Double);
++
++ valueChangedSpy.wait(signalSpyTimeout);
++ QCOMPARE(valueChangedSpy.size(), 1);
++ QCOMPARE(valueChangedSpy.at(0).at(1).value<quint32>(), 3U);
++
++ modifySpy.clear();
++ triggeringNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::TriggeredItemIds,
++ QVariant::fromValue(QSet<quint32>()));
++ modifySpy.wait();
++ QCOMPARE(modifySpy.size(), 1);
++ QCOMPARE(modifySpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(triggeredNode->monitoringStatus(QOpcUa::NodeAttribute::Value).triggeredItemIds(), QSet<quint32>());
++
++ // There seems to be a slight delay when removing the link with open62541 1.4
++ for (int i = 0; i < 10; ++i) {
++ valueChangedSpy.clear();
++ WRITE_VALUE_ATTRIBUTE(writeNode, 4 + i, QOpcUa::Types::Int32);
++
++ valueChangedSpy.wait(1000);
++ if (valueChangedSpy.empty())
++ break;
++
++ QTest::qWait(10);
++ }
++
++ QCOMPARE(valueChangedSpy.size(), 0);
++
++ WRITE_VALUE_ATTRIBUTE(triggeringNode, 1237.0, QOpcUa::Types::Double);
++
++ valueChangedSpy.wait(1000);
++ QCOMPARE(valueChangedSpy.size(), 0);
++ }
++
++ void Tst_QOpcUaClient::methodCall()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QList<QOpcUa::TypedVariant> args;
++ for (int i = 0; i < 2; i++)
++ args.push_back(QOpcUa::TypedVariant(double(4), QOpcUa::Double));
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=TestFolder"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy methodSpy(node.data(), &QOpcUaNode::methodCallFinished);
++
++ bool success = node->callMethod(u"ns=3;s=Test.Method.Multiply"_s, args);
++ QVERIFY(success == true);
++
++ methodSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(methodSpy.size(), 1);
++ QCOMPARE(methodSpy.at(0).at(0).value<QString>(), u"ns=3;s=Test.Method.Multiply"_s);
++ QCOMPARE(methodSpy.at(0).at(1).value<double>(), 16.0);
++ QCOMPARE(QOpcUa::isSuccessStatus(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>()), true);
++ }
++
++ void Tst_QOpcUaClient::methodCallInvalid()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QList<QOpcUa::TypedVariant> args;
++ for (int i = 0; i < 3; i++)
++ args.push_back(QOpcUa::TypedVariant(double(4), QOpcUa::Double));
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=TestFolder"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy methodSpy(node.data(), &QOpcUaNode::methodCallFinished);
++
++ bool success = node->callMethod(u"ns=3;s=Test.Method.Divide"_s, args); // Does not exist
++ QVERIFY(success == true);
++ methodSpy.wait(signalSpyTimeout);
++ QCOMPARE(methodSpy.size(), 1);
++ QCOMPARE(QOpcUa::errorCategory(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>()), QOpcUa::ErrorCategory::NodeError);
++
++ methodSpy.clear();
++ success = node->callMethod(u"ns=3;s=Test.Method.Multiply"_s, args); // One excess argument
++ QVERIFY(success == true);
++ methodSpy.wait(signalSpyTimeout);
++ QCOMPARE(methodSpy.size(), 1);
++ QCOMPARE(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadTooManyArguments);
++
++ methodSpy.clear();
++ args.resize(1);
++ success = node->callMethod(u"ns=3;s=Test.Method.Multiply"_s, args); // One argument missing
++ QVERIFY(success == true);
++ methodSpy.wait(signalSpyTimeout);
++ QCOMPARE(methodSpy.size(), 1);
++ QCOMPARE(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadArgumentsMissing);
++ }
++
++ void Tst_QOpcUaClient::readMethodArguments()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(
++ opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server_GetMonitoredItems_InputArguments)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++
++ QOpcUaArgument argument = node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaArgument>();
++ QCOMPARE(argument.name(), u"SubscriptionId"_s);
++ QCOMPARE(argument.dataTypeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::UInt32));
++ QCOMPARE(argument.valueRank(), -1);
++ QVERIFY(argument.arrayDimensions().isEmpty());
++ QCOMPARE(argument.description(), QOpcUaLocalizedText());
++
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server_GetMonitoredItems_OutputArguments)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++
++ QVariantList list = node->attribute(QOpcUa::NodeAttribute::Value).toList();
++ QCOMPARE(list.size(), 2);
++
++ argument = list.at(0).value<QOpcUaArgument>();
++ QCOMPARE(argument.name(), u"ServerHandles"_s);
++ QCOMPARE(argument.dataTypeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::UInt32));
++ QCOMPARE(argument.valueRank(), 1);
++ QCOMPARE(argument.arrayDimensions().size(), 1);
++ QCOMPARE(argument.description(), QOpcUaLocalizedText());
++
++ argument = list.at(1).value<QOpcUaArgument>();
++ QCOMPARE(argument.name(), u"ClientHandles"_s);
++ QCOMPARE(argument.dataTypeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::UInt32));
++ QCOMPARE(argument.valueRank(), 1);
++ QCOMPARE(argument.arrayDimensions().size(), 1);
++ QCOMPARE(argument.description(), QOpcUaLocalizedText());
++ }
++
++ void Tst_QOpcUaClient::malformedNodeString()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> invalidNode(opcuaClient->node(u"justsomerandomstring"_s));
++ QVERIFY(invalidNode == nullptr);
++
++ invalidNode.reset(opcuaClient->node(u"ns=a;i=b"_s));
++ QVERIFY(invalidNode == nullptr);
++
++ invalidNode.reset(opcuaClient->node(u"ns=;i="_s));
++ QVERIFY(invalidNode == nullptr);
++
++ invalidNode.reset(opcuaClient->node(u"ns=0;x=123"_s));
++ QVERIFY(invalidNode == nullptr);
++
++ invalidNode.reset(opcuaClient->node(u"ns=0,i=31;"_s));
++ QVERIFY(invalidNode == nullptr);
++
++ invalidNode.reset(opcuaClient->node(u"ns:0;i:31;"_s));
++ QVERIFY(invalidNode == nullptr);
++ }
++
++ void Tst_QOpcUaClient::nodeIdGeneration()
++ {
++ QString nodeId = QOpcUa::nodeIdFromString(1, u"TestString"_s);
++ QCOMPARE(nodeId, u"ns=1;s=TestString"_s);
++ nodeId = QOpcUa::nodeIdFromInteger(1, 10);
++ QCOMPARE(nodeId, u"ns=1;i=10"_s);
++ nodeId = QOpcUa::nodeIdFromGuid(1, QUuid("08081e75-8e5e-319b-954f-f3a7613dc29b"));
++ QCOMPARE(nodeId, u"ns=1;g=08081e75-8e5e-319b-954f-f3a7613dc29b"_s);
++ nodeId = QOpcUa::nodeIdFromByteString(1, QByteArray::fromBase64("UXQgZnR3IQ=="));
++ QCOMPARE(nodeId, u"ns=1;b=UXQgZnR3IQ=="_s);
++ nodeId = QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::HasComponent);
++ QCOMPARE(nodeId, QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::HasComponent));
++ }
++
++ void Tst_QOpcUaClient::multipleClients()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector1(opcuaClient, m_endpoint);
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(readWriteNode));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, 42.0, QOpcUa::Types::Double);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).toDouble(), 42.0);
++
++ QScopedPointer<QOpcUaClient> b(m_opcUa.createClient(opcuaClient->backend()));
++ OpcuaConnector connector2(b.get(), m_endpoint);
++ node.reset(b->node(readWriteNode));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).toDouble(), 42.0);
++
++ QScopedPointer<QOpcUaClient> d(m_opcUa.createClient(opcuaClient->backend()));
++ OpcuaConnector connector3(d.get(), m_endpoint);
++ node.reset(d->node(readWriteNode));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).toDouble(), 42.0);
++ }
++
++ void Tst_QOpcUaClient::nodeClass()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // Root -> Types -> ReferenceTypes -> References
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::References)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::ReferenceType);
++
++ // Root -> Types -> DataTypes -> BaseDataTypes -> Boolean
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Boolean)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::DataType);
++
++ // Root -> Types -> DataTypes -> ObjectTypes -> BaseObjectTypes -> FolderType
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::FolderType)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::ObjectType);
++
++ // Root -> Types -> DataTypes -> VariableTypes -> BaseVariableType -> PropertyType
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::PropertyType)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::VariableType);
++
++ // Root -> Objects
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::ObjectsFolder)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::Object);
++
++ // Root -> Objects -> Server -> NamespaceArray
++ node.reset(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server_NamespaceArray)));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node)
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(), QOpcUa::NodeClass::Variable);
++ }
++
++ void Tst_QOpcUaClient::writeReadArray()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QVariantList list;
++
++ list.append(true);
++ list.append(false);
++ list.append(true);
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Boolean"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Boolean);
++
++ list.clear();
++ list.append(std::numeric_limits<quint8>::min());
++ list.append((std::numeric_limits<quint8>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Byte"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Byte);
++
++ list.clear();
++ list.append(QDateTime::currentDateTime());
++ list.append(QDateTime::currentDateTime());
++ list.append(QDateTime::currentDateTime());
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DateTime"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::DateTime);
++
++ list.clear();
++ list.append(23.5);
++ list.append(23.6);
++ list.append(23.7);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Double"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Double);
++
++ list.clear();
++ list.append(23.5);
++ list.append(23.6);
++ list.append(23.7);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Float"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Float);
++
++ list.clear();
++ list.append(std::numeric_limits<qint16>::min());
++ list.append((std::numeric_limits<qint16>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int16"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Int16);
++
++ list.clear();
++ list.append(std::numeric_limits<qint32>::min());
++ list.append((std::numeric_limits<qint32>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int32"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Int32);
++
++ list.clear();
++ list.append(std::numeric_limits<qint64>::min());
++ list.append((std::numeric_limits<qint64>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int64"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Int64);
++
++ list.clear();
++ list.append(std::numeric_limits<qint8>::min());
++ list.append((std::numeric_limits<qint8>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.SByte"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::SByte);
++
++ list.clear();
++ list.append(u"Test1"_s);
++ list.append(u"Test2"_s);
++ list.append(u"Test3"_s);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.String"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::String);
++
++ list.clear();
++ list.append(std::numeric_limits<quint16>::min());
++ list.append((std::numeric_limits<quint16>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt16"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::UInt16);
++
++ list.clear();
++ list.append(std::numeric_limits<quint32>::min());
++ list.append((std::numeric_limits<quint32>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt32"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::UInt32);
++
++ list.clear();
++ list.append(std::numeric_limits<quint64>::min());
++ list.append((std::numeric_limits<quint64>::max)());
++ list.append(10);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt64"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::UInt64);
++
++ list.clear();
++ list.append(localizedTexts[0]);
++ list.append(localizedTexts[1]);
++ list.append(localizedTexts[2]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.LocalizedText"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::LocalizedText);
++
++ list.clear();
++ list.append(u"abc"_s);
++ list.append(u"def"_s);
++ list.append(withNull);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ByteString"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::ByteString);
++
++ list.clear();
++ list.append(testUuid[0]);
++ list.append(testUuid[1]);
++ list.append(testUuid[2]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Guid"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Guid);
++
++ list.clear();
++ list.append(u"ns=0;i=0"_s);
++ list.append(u"ns=0;i=1"_s);
++ list.append(u"ns=0;i=2"_s);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.NodeId"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::NodeId);
++
++ list.clear();
++ list.append(QOpcUaQualifiedName(0, u"Test0"_s));
++ list.append(QOpcUaQualifiedName(1, u"Test1"_s));
++ list.append(QOpcUaQualifiedName(2, u"Test2"_s));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.QualifiedName"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::QualifiedName);
++
++ list.clear();
++ list.append(QOpcUa::UaStatusCode::Good);
++ list.append(QOpcUa::UaStatusCode::BadUnexpectedError);
++ list.append(QOpcUa::UaStatusCode::BadInternalError);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StatusCode"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Types::StatusCode);
++
++ list.clear();
++ list.append(QVariant::fromValue(testRanges[0]));
++ list.append(QVariant::fromValue(testRanges[1]));
++ list.append(QVariant::fromValue(testRanges[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Range"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Range);
++
++ list.clear();
++ list.append(QVariant::fromValue(testEUInfos[0]));
++ list.append(QVariant::fromValue(testEUInfos[1]));
++ list.append(QVariant::fromValue(testEUInfos[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EUInformation"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::EUInformation);
++
++ list.clear();
++ list.append(QVariant::fromValue(testComplex[0]));
++ list.append(QVariant::fromValue(testComplex[1]));
++ list.append(QVariant::fromValue(testComplex[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::ComplexNumber);
++
++ list.clear();
++ list.append(QVariant::fromValue(testDoubleComplex[0]));
++ list.append(QVariant::fromValue(testDoubleComplex[1]));
++ list.append(QVariant::fromValue(testDoubleComplex[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DoubleComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::DoubleComplexNumber);
++
++ list.clear();
++ list.append(QVariant::fromValue(testAxisInfo[0]));
++ list.append(QVariant::fromValue(testAxisInfo[1]));
++ list.append(QVariant::fromValue(testAxisInfo[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.AxisInformation"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::AxisInformation);
++
++ list.clear();
++ list.append(QVariant::fromValue(testXV[0]));
++ list.append(QVariant::fromValue(testXV[1]));
++ list.append(QVariant::fromValue(testXV[2]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.XV"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::XV);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ExtensionObject"_s));
++ QVERIFY(node != nullptr);
++
++ QVariantList value;
++
++ for (int i = 0; i < testRanges.size(); ++i) {
++ QOpcUaExtensionObject obj;
++ ENCODE_EXTENSION_OBJECT(obj, i);
++ value.append(obj);
++ }
++
++ WRITE_VALUE_ATTRIBUTE(node, value, QOpcUa::Types::ExtensionObject); // Write value to check for
++
++ list.clear();
++ list.append(xmlElements[0]);
++ list.append(xmlElements[1]);
++ list.append(xmlElements[2]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.XmlElement"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::XmlElement);
++
++ list.clear();
++ list.append(testExpandedNodeId[0]);
++ list.append(testExpandedNodeId[1]);
++ list.append(testExpandedNodeId[2]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ExpandedNodeId"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::ExpandedNodeId);
++
++ list.clear();
++ list.append(testArguments[0]);
++ list.append(testArguments[1]);
++ list.append(testArguments[2]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Argument"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Argument);
++
++ list.clear();
++ list.append(testStructureFields[0]);
++ list.append(testStructureFields[1]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StructureField"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::StructureField);
++
++ list.clear();
++ list.append(testStructureDefinitions[0]);
++ list.append(testStructureDefinitions[1]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StructureDefinition"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::StructureDefinition);
++
++ list.clear();
++ list.append(testEnumFields[0]);
++ list.append(testEnumFields[1]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EnumField"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::EnumField);
++
++ list.clear();
++ list.append(testEnumDefinitions[0]);
++ list.append(testEnumDefinitions[1]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EnumDefinition"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::EnumDefinition);
++
++ list.clear();
++ list.append(testDiagnosticInfos[0]);
++ list.append(testDiagnosticInfos[1]);
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DiagnosticInfo"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::DiagnosticInfo);
++
++ list.clear();
++ list.append(QVariant::fromValue(testEventFilters[0]));
++ list.append(QVariant::fromValue(testEventFilters[1]));
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EventFilter"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::EventFilter);
++
++ // Writing finished, attempt to read back
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Boolean"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant booleanArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(booleanArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(booleanArray.toList().size(), 3);
++ QCOMPARE(booleanArray.toList()[0].metaType().id(), QMetaType::Bool);
++ QCOMPARE(booleanArray.toList()[0].toBool(), true);
++ QCOMPARE(booleanArray.toList()[1].toBool(), false);
++ QCOMPARE(booleanArray.toList()[2].toBool(), true);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int32"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int32Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(int32Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(int32Array.toList().size(), 3);
++ QCOMPARE(int32Array.toList()[0].metaType().id(), QMetaType::Int);
++ QCOMPARE(int32Array.toList()[0].toInt(), std::numeric_limits<qint32>::min());
++ QCOMPARE(int32Array.toList()[1].toInt(), (std::numeric_limits<qint32>::max)());
++ QCOMPARE(int32Array.toList()[2].toInt(), 10);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt32"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint32Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(uint32Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(uint32Array.toList().size(), 3);
++ QCOMPARE(uint32Array.toList()[0].metaType().id(), QMetaType::UInt);
++ QCOMPARE(uint32Array.toList()[0].toUInt(), std::numeric_limits<quint32>::min());
++ QCOMPARE(uint32Array.toList()[1].toUInt(), (std::numeric_limits<quint32>::max)());
++ QCOMPARE(uint32Array.toList()[2].toUInt(), quint32(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Double"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant doubleArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(doubleArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(doubleArray.toList().size(), 3);
++ QCOMPARE(doubleArray.toList()[0].metaType().id(), QMetaType::Double);
++ QCOMPARE(doubleArray.toList()[0].toDouble(), double(23.5));
++ QCOMPARE(doubleArray.toList()[1].toDouble(), double(23.6));
++ QCOMPARE(doubleArray.toList()[2].toDouble(), double(23.7));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Float"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant floatArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(floatArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(floatArray.toList().size(), 3);
++ QCOMPARE(floatArray.toList()[0].userType(), QMetaType::Float);
++ QCOMPARE(floatArray.toList()[0].toFloat(), float(23.5));
++ QCOMPARE(floatArray.toList()[1].toFloat(), float(23.6));
++ QCOMPARE(floatArray.toList()[2].toFloat(), float(23.7));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.String"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant stringArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(stringArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(stringArray.toList().size(), 3);
++ QCOMPARE(stringArray.toList()[0].metaType().id(), QMetaType::QString);
++ QCOMPARE(stringArray.toList()[0].toString(), u"Test1"_s);
++ QCOMPARE(stringArray.toList()[1].toString(), u"Test2"_s);
++ QCOMPARE(stringArray.toList()[2].toString(), u"Test3"_s);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DateTime"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant dateTimeArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(dateTimeArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(dateTimeArray.toList().size(), 3);
++ QCOMPARE(dateTimeArray.toList()[0].metaType().id(), QMetaType::QDateTime);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.LocalizedText"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant ltArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(ltArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(ltArray.toList().size(), 3);
++ QCOMPARE(ltArray.toList()[0].value<QOpcUaLocalizedText>(), localizedTexts[0]);
++ QCOMPARE(ltArray.toList()[1].value<QOpcUaLocalizedText>(), localizedTexts[1]);
++ QCOMPARE(ltArray.toList()[2].value<QOpcUaLocalizedText>(), localizedTexts[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt16"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint16Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(uint16Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(uint16Array.toList().size(), 3);
++ QCOMPARE(uint16Array.toList()[0].userType(), QMetaType::UShort);
++ QCOMPARE(uint16Array.toList()[0], std::numeric_limits<quint16>::min());
++ QCOMPARE(uint16Array.toList()[1], (std::numeric_limits<quint16>::max)());
++ QCOMPARE(uint16Array.toList()[2], quint16(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int16"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int16Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(int16Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(int16Array.toList().size(), 3);
++ QCOMPARE(int16Array.toList()[0].userType(), QMetaType::Short);
++ QCOMPARE(int16Array.toList()[0], std::numeric_limits<qint16>::min());
++ QCOMPARE(int16Array.toList()[1], (std::numeric_limits<qint16>::max)());
++ QCOMPARE(int16Array.toList()[2], qint16(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.UInt64"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint64Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(uint64Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(uint64Array.toList().size(), 3);
++ QCOMPARE(uint64Array.toList()[0].metaType().id(), QMetaType::ULongLong);
++ QCOMPARE(uint64Array.toList()[0], std::numeric_limits<quint64>::min());
++ QCOMPARE(uint64Array.toList()[1], (std::numeric_limits<quint64>::max)());
++ QCOMPARE(uint64Array.toList()[2], quint64(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int64"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int64Array = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(int64Array.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(int64Array.toList().size(), 3);
++ QCOMPARE(int64Array.toList()[0].metaType().id(), QMetaType::LongLong);
++ QCOMPARE(int64Array.toList()[0], std::numeric_limits<qint64>::min());
++ QCOMPARE(int64Array.toList()[1], (std::numeric_limits<qint64>::max)());
++ QCOMPARE(int64Array.toList()[2], qint64(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Byte"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant byteArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(byteArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(byteArray.toList().size(), 3);
++ QCOMPARE(byteArray.toList()[0].userType(), QMetaType::UChar);
++ QCOMPARE(byteArray.toList()[0], std::numeric_limits<quint8>::min());
++ QCOMPARE(byteArray.toList()[1], (std::numeric_limits<quint8>::max)());
++ QCOMPARE(byteArray.toList()[2], quint8(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ByteString"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant byteStringArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(byteStringArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(byteStringArray.toList().size(), 3);
++ QCOMPARE(byteStringArray.toList()[0].userType(), QMetaType::QByteArray);
++ QCOMPARE(byteStringArray.toList()[0].value<QByteArray>(), "abc");
++ QCOMPARE(byteStringArray.toList()[1].value<QByteArray>(), "def");
++ QCOMPARE(byteStringArray.toList()[2].value<QByteArray>(), withNull);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Guid"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant guidArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(guidArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(guidArray.toList().size(), 3);
++ QCOMPARE(guidArray.toList()[0].toUuid(), testUuid[0]);
++ QCOMPARE(guidArray.toList()[1].toUuid(), testUuid[1]);
++ QCOMPARE(guidArray.toList()[2].toUuid(), testUuid[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.SByte"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant sbyteArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(sbyteArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(sbyteArray.toList().size(), 3);
++ QCOMPARE(sbyteArray.toList()[0].userType(), QMetaType::SChar);
++ QCOMPARE(sbyteArray.toList()[0], std::numeric_limits<qint8>::min());
++ QCOMPARE(sbyteArray.toList()[1], (std::numeric_limits<qint8>::max)());
++ QCOMPARE(sbyteArray.toList()[2], qint8(10));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.NodeId"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant nodeIdArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(nodeIdArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(nodeIdArray.toList().size(), 3);
++ QCOMPARE(nodeIdArray.toList()[0].metaType().id(), QMetaType::QString);
++ QCOMPARE(nodeIdArray.toList()[0].toString(), u"ns=0;i=0"_s);
++ QCOMPARE(nodeIdArray.toList()[1].toString(), u"ns=0;i=1"_s);
++ QCOMPARE(nodeIdArray.toList()[2].toString(), u"ns=0;i=2"_s);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.QualifiedName"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant qualifiedNameArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(qualifiedNameArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(qualifiedNameArray.toList().size(), 3);
++ QCOMPARE(qualifiedNameArray.toList()[0].value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(0, u"Test0"_s));
++ QCOMPARE(qualifiedNameArray.toList()[1].value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(1, u"Test1"_s));
++ QCOMPARE(qualifiedNameArray.toList()[2].value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(2, u"Test2"_s));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StatusCode"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant statusCodeArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(statusCodeArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(statusCodeArray.toList().size(), 3);
++ QCOMPARE(statusCodeArray.toList()[0].value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(statusCodeArray.toList()[1].value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadUnexpectedError);
++ QCOMPARE(statusCodeArray.toList()[2].value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadInternalError);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Range"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant rangeArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(rangeArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(rangeArray.toList().size(), 3);
++ QCOMPARE(rangeArray.toList()[0].value<QOpcUaRange>(), testRanges[0]);
++ QCOMPARE(rangeArray.toList()[1].value<QOpcUaRange>(), testRanges[1]);
++ QCOMPARE(rangeArray.toList()[2].value<QOpcUaRange>(), testRanges[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EUInformation"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant euiArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(euiArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(euiArray.toList().size(), 3);
++ QCOMPARE(euiArray.toList()[0].value<QOpcUaEUInformation>(), testEUInfos[0]);
++ QCOMPARE(euiArray.toList()[1].value<QOpcUaEUInformation>(), testEUInfos[1]);
++ QCOMPARE(euiArray.toList()[2].value<QOpcUaEUInformation>(), testEUInfos[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant complexArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(complexArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(complexArray.toList().size(), 3);
++ QCOMPARE(complexArray.toList()[0].value<QOpcUaComplexNumber>(), testComplex[0]);
++ QCOMPARE(complexArray.toList()[1].value<QOpcUaComplexNumber>(), testComplex[1]);
++ QCOMPARE(complexArray.toList()[2].value<QOpcUaComplexNumber>(), testComplex[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DoubleComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant doubleComplexArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(doubleComplexArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(doubleComplexArray.toList().size(), 3);
++ QCOMPARE(doubleComplexArray.toList()[0].value<QOpcUaDoubleComplexNumber>(), testDoubleComplex[0]);
++ QCOMPARE(doubleComplexArray.toList()[1].value<QOpcUaDoubleComplexNumber>(), testDoubleComplex[1]);
++ QCOMPARE(doubleComplexArray.toList()[2].value<QOpcUaDoubleComplexNumber>(), testDoubleComplex[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.AxisInformation"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant axisInfoArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(axisInfoArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(axisInfoArray.toList().size(), 3);
++
++ QCOMPARE(axisInfoArray.toList()[0].value<QOpcUaAxisInformation>(), testAxisInfo[0]);
++ QCOMPARE(axisInfoArray.toList()[1].value<QOpcUaAxisInformation>(), testAxisInfo[1]);
++ QCOMPARE(axisInfoArray.toList()[2].value<QOpcUaAxisInformation>(), testAxisInfo[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.XV"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant xVArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(xVArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(xVArray.toList().size(), 3);
++ QCOMPARE(xVArray.toList()[0].value<QOpcUaXValue>(), testXV[0]);
++ QCOMPARE(xVArray.toList()[1].value<QOpcUaXValue>(), testXV[1]);
++ QCOMPARE(xVArray.toList()[2].value<QOpcUaXValue>(), testXV[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ExtensionObject"_s));
++ QVERIFY(node != nullptr);
++
++ READ_MANDATORY_VARIABLE_NODE(node);
++
++ list = node->attribute(QOpcUa::NodeAttribute::Value).toList();
++ QCOMPARE(list.size(), testRanges.size());
++
++ for (int i = 0; i < testRanges.size(); ++i) {
++ QOpcUaExtensionObject obj = list.at(i).value<QOpcUaExtensionObject>();
++ VERIFY_EXTENSION_OBJECT(obj, i);
++ }
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.XmlElement"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant xmlElementArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(xmlElementArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(xmlElementArray.toList().size(), 3);
++ QCOMPARE(xmlElementArray.toList()[0].metaType().id(), QMetaType::QString);
++ QCOMPARE(xmlElementArray.toList()[0].toString(), xmlElements[0]);
++ QCOMPARE(xmlElementArray.toList()[1].toString(), xmlElements[1]);
++ QCOMPARE(xmlElementArray.toList()[2].toString(), xmlElements[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.ExpandedNodeId"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant expandedNodeIdArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(expandedNodeIdArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(expandedNodeIdArray.toList().size(), 3);
++ QCOMPARE(expandedNodeIdArray.toList()[0].value<QOpcUaExpandedNodeId>(), testExpandedNodeId[0]);
++ QCOMPARE(expandedNodeIdArray.toList()[1].value<QOpcUaExpandedNodeId>(), testExpandedNodeId[1]);
++ QCOMPARE(expandedNodeIdArray.toList()[2].value<QOpcUaExpandedNodeId>(), testExpandedNodeId[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Argument"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant argumentArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(argumentArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(argumentArray.toList().size(), 3);
++ QCOMPARE(argumentArray.toList()[0].value<QOpcUaArgument>(), testArguments[0]);
++ QCOMPARE(argumentArray.toList()[1].value<QOpcUaArgument>(), testArguments[1]);
++ QCOMPARE(argumentArray.toList()[2].value<QOpcUaArgument>(), testArguments[2]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StructureField"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant structureFieldsArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(structureFieldsArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(structureFieldsArray.toList().length(), 2);
++ QCOMPARE(structureFieldsArray.toList()[0].value<QOpcUaStructureField>(), testStructureFields[0]);
++ QCOMPARE(structureFieldsArray.toList()[1].value<QOpcUaStructureField>(), testStructureFields[1]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.StructureDefinition"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant structureDefinitionsArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(structureDefinitionsArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(structureDefinitionsArray.toList().length(), 2);
++ QCOMPARE(structureDefinitionsArray.toList()[0].value<QOpcUaStructureDefinition>(), testStructureDefinitions[0]);
++ QCOMPARE(structureDefinitionsArray.toList()[1].value<QOpcUaStructureDefinition>(), testStructureDefinitions[1]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EnumField"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant enumFieldsArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(enumFieldsArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(enumFieldsArray.toList().length(), 2);
++ QCOMPARE(enumFieldsArray.toList()[0].value<QOpcUaEnumField>(), testEnumFields[0]);
++ QCOMPARE(enumFieldsArray.toList()[1].value<QOpcUaEnumField>(), testEnumFields[1]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EnumDefinition"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant enumDefinitionsArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(enumDefinitionsArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(enumDefinitionsArray.toList().length(), 2);
++ QCOMPARE(enumDefinitionsArray.toList()[0].value<QOpcUaEnumDefinition>(), testEnumDefinitions[0]);
++ QCOMPARE(enumDefinitionsArray.toList()[1].value<QOpcUaEnumDefinition>(), testEnumDefinitions[1]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.DiagnosticInfo"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant diagnosticInfoArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(diagnosticInfoArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(diagnosticInfoArray.toList().length(), 2);
++ QCOMPARE(diagnosticInfoArray.toList()[0].value<QOpcUaDiagnosticInfo>(), testDiagnosticInfos[0]);
++ QCOMPARE(diagnosticInfoArray.toList()[1].value<QOpcUaDiagnosticInfo>(), testDiagnosticInfos[1]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.EventFilter"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant eventFilterArray = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(eventFilterArray.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(eventFilterArray.toList().size(), 2);
++ QCOMPARE(eventFilterArray.toList()[0].value<QOpcUaMonitoringParameters::EventFilter>(), testEventFilters[0]);
++ QCOMPARE(eventFilterArray.toList()[1].value<QOpcUaMonitoringParameters::EventFilter>(), testEventFilters[1]);
++ }
++
++ void Tst_QOpcUaClient::writeReadScalar()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Boolean"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, true, QOpcUa::Types::Boolean);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, std::numeric_limits<qint32>::min(), QOpcUa::Types::Int32);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt32"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, (std::numeric_limits<quint32>::max)(), QOpcUa::UInt32);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, 42, QOpcUa::Double);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Float"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, 42, QOpcUa::Float);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.String"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, u"QOpcUa Teststring"_s, QOpcUa::String);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DateTime"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QDateTime::currentDateTime(), QOpcUa::DateTime);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt16"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, (std::numeric_limits<quint16>::max)(), QOpcUa::UInt16);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int16"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, std::numeric_limits<qint16>::min(), QOpcUa::Int16);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt64"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, (std::numeric_limits<quint64>::max)(), QOpcUa::UInt64);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int64"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, std::numeric_limits<qint64>::min(), QOpcUa::Int64);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Byte"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, (std::numeric_limits<quint8>::max)(), QOpcUa::Byte);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.SByte"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, std::numeric_limits<qint8>::min(), QOpcUa::SByte);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.LocalizedText"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, localizedTexts[0], QOpcUa::LocalizedText);
++
++ QVariant data = withNull;
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ByteString"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, data, QOpcUa::ByteString);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Guid"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testUuid[0], QOpcUa::Guid);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.NodeId"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, u"ns=1;s=Test"_s, QOpcUa::NodeId);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.QualifiedName"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QOpcUaQualifiedName(0, u"Test0"_s), QOpcUa::QualifiedName);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StatusCode"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QOpcUa::UaStatusCode::BadInternalError, QOpcUa::StatusCode);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Range"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testRanges[0]), QOpcUa::Range);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EUInformation"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testEUInfos[0]), QOpcUa::EUInformation);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testComplex[0]), QOpcUa::ComplexNumber);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DoubleComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testDoubleComplex[0]), QOpcUa::DoubleComplexNumber);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.AxisInformation"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testAxisInfo[0]), QOpcUa::AxisInformation);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.XV"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, QVariant::fromValue(testXV[0]), QOpcUa::XV);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ExtensionObject"_s));
++ QVERIFY(node != nullptr);
++
++ QOpcUaExtensionObject obj;
++ ENCODE_EXTENSION_OBJECT(obj, 0);
++
++ WRITE_VALUE_ATTRIBUTE(node, obj, QOpcUa::Types::ExtensionObject); // Write value to check for
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.XmlElement"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, xmlElements[0], QOpcUa::XmlElement);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ExpandedNodeId"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testExpandedNodeId[0], QOpcUa::ExpandedNodeId);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Argument"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testArguments[0], QOpcUa::Argument);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StructureField"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testStructureFields[0], QOpcUa::StructureField);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StructureDefinition"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testStructureDefinitions[0], QOpcUa::StructureDefinition);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EnumField"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testEnumFields[0], QOpcUa::EnumField);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EnumDefinition"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testEnumDefinitions[0], QOpcUa::EnumDefinition);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DiagnosticInfo"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testDiagnosticInfos[0], QOpcUa::DiagnosticInfo);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EventFilter"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, testEventFilters[0], QOpcUa::EventFilter);
++
++ // Writing finished, attempt to read back
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Boolean"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant booleanScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(booleanScalar.isValid());
++ QCOMPARE(booleanScalar.metaType().id(), QMetaType::Bool);
++ QCOMPARE(booleanScalar.toBool(), true);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int32"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int32Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(int32Scalar.isValid());
++ QCOMPARE(int32Scalar.metaType().id(), QMetaType::Int);
++ QCOMPARE(int32Scalar.toInt(), std::numeric_limits<qint32>::min());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt32"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint32Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(uint32Scalar.isValid());
++ QCOMPARE(uint32Scalar.metaType().id(), QMetaType::UInt);
++ QCOMPARE(uint32Scalar.toUInt(), (std::numeric_limits<quint32>::max)());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant doubleScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(doubleScalar.isValid());
++ QCOMPARE(doubleScalar.metaType().id(), QMetaType::Double);
++ QCOMPARE(doubleScalar.toDouble(), double(42));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Float"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant floatScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(floatScalar.isValid());
++ QCOMPARE(floatScalar.userType(), QMetaType::Float);
++ QCOMPARE(floatScalar.toFloat(), float(42));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.String"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant stringScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(stringScalar.isValid());
++ QCOMPARE(stringScalar.metaType().id(), QMetaType::QString);
++ QCOMPARE(stringScalar.toString(), u"QOpcUa Teststring"_s);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DateTime"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant dateTimeScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(dateTimeScalar.metaType().id(), QMetaType::QDateTime);
++ QVERIFY(dateTimeScalar.isValid());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.LocalizedText"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant ltScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(ltScalar.isValid());
++ QCOMPARE(ltScalar.value<QOpcUaLocalizedText>(), localizedTexts[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt16"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint16Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(uint16Scalar.isValid());
++ QCOMPARE(uint16Scalar.userType(), QMetaType::UShort);
++ QCOMPARE(uint16Scalar, (std::numeric_limits<quint16>::max)());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int16"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int16Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(int16Scalar.isValid());
++ QCOMPARE(int16Scalar.userType(), QMetaType::Short);
++ QCOMPARE(int16Scalar, std::numeric_limits<qint16>::min());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.UInt64"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant uint64Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(uint64Scalar.isValid());
++ QCOMPARE(uint64Scalar.metaType().id(), QMetaType::ULongLong);
++ QCOMPARE(uint64Scalar, (std::numeric_limits<quint64>::max)());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Int64"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant int64Scalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(int64Scalar.isValid());
++ QCOMPARE(int64Scalar.metaType().id(), QMetaType::LongLong);
++ QCOMPARE(int64Scalar, std::numeric_limits<qint64>::min());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Byte"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant byteScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(byteScalar.isValid());
++ QCOMPARE(byteScalar.userType(), QMetaType::UChar);
++ QCOMPARE(byteScalar, (std::numeric_limits<quint8>::max)());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.SByte"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant sbyteScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(sbyteScalar.isValid());
++ QCOMPARE(sbyteScalar.userType(), QMetaType::SChar);
++ QCOMPARE(sbyteScalar, std::numeric_limits<qint8>::min());
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ByteString"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant byteStringScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(byteStringScalar.isValid());
++ QCOMPARE(byteStringScalar.userType(), QMetaType::QByteArray);
++ QCOMPARE(byteStringScalar, withNull);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Guid"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant guidScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(guidScalar.isValid());
++ QCOMPARE(guidScalar.userType(), QMetaType::QUuid);
++ QCOMPARE(guidScalar.toUuid(), testUuid[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.NodeId"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant nodeIdScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(nodeIdScalar.isValid());
++ QCOMPARE(nodeIdScalar.metaType().id(), QMetaType::QString);
++ QCOMPARE(nodeIdScalar.toString(), u"ns=1;s=Test"_s);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.QualifiedName"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant qualifiedNameScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(qualifiedNameScalar.value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(0, u"Test0"_s));
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StatusCode"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant statusCodeScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(statusCodeScalar.isValid());
++ QCOMPARE(statusCodeScalar.value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadInternalError);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Range"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaRange>(), testRanges[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EUInformation"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaEUInformation>(), testEUInfos[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaComplexNumber>(), testComplex[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DoubleComplexNumber"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaDoubleComplexNumber>(), testDoubleComplex[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.AxisInformation"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaAxisInformation>(), testAxisInfo[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.XV"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaXValue>(), testXV[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ExtensionObject"_s));
++ QVERIFY(node != nullptr);
++
++ READ_MANDATORY_VARIABLE_NODE(node);
++
++ obj = node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaExtensionObject>();
++ VERIFY_EXTENSION_OBJECT(obj, 0);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.XmlElement"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant xmlElementScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(xmlElementScalar.isValid());
++ QCOMPARE(xmlElementScalar.metaType().id(), QMetaType::QString);
++ QCOMPARE(xmlElementScalar.toString(), xmlElements[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.ExpandedNodeId"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant expandedNodeIdScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(expandedNodeIdScalar.isValid());
++ QCOMPARE(expandedNodeIdScalar.value<QOpcUaExpandedNodeId>(), testExpandedNodeId[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Argument"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant argumentScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(argumentScalar.isValid());
++ QCOMPARE(argumentScalar.value<QOpcUaArgument>(), testArguments[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StructureField"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant structureFieldScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(structureFieldScalar.isValid());
++ QCOMPARE(structureFieldScalar.value<QOpcUaStructureField>(), testStructureFields[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.StructureDefinition"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant structureDefinitionScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(structureDefinitionScalar.isValid());
++ QCOMPARE(structureDefinitionScalar.value<QOpcUaStructureDefinition>(), testStructureDefinitions[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EnumField"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant enumFieldScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(enumFieldScalar.isValid());
++ QCOMPARE(enumFieldScalar.value<QOpcUaEnumField>(), testEnumFields[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EnumDefinition"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant enumDefinitionScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(enumDefinitionScalar.isValid());
++ QCOMPARE(enumDefinitionScalar.value<QOpcUaEnumDefinition>(), testEnumDefinitions[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DiagnosticInfo"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ const QVariant diagnosticInfoScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(diagnosticInfoScalar.isValid());
++ QCOMPARE(diagnosticInfoScalar.value<QOpcUaDiagnosticInfo>(), testDiagnosticInfos[0]);
++
++ node.reset(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.EventFilter"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node)
++ QVariant eventFilterScalar = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(eventFilterScalar.isValid());
++ QCOMPARE(eventFilterScalar.value<QOpcUaMonitoringParameters::EventFilter>(), testEventFilters[0]);
++ }
++
++ void Tst_QOpcUaClient::readReencodedExtensionObject()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ const auto checkAndDecodeFirstMember = [](const QOpcUaExtensionObject &obj, int index) {
++ QCOMPARE(obj.encoding(), QOpcUaExtensionObject::Encoding::ByteString);
++ QCOMPARE(obj.encodingTypeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::ServerStatusDataType_Encoding_DefaultBinary));
++ QCOMPARE(obj.encodedBody().size(), 140);
++ auto data = obj.encodedBody();
++ auto decoder = QOpcUaBinaryDataEncoding(&data);
++ bool success = false;
++ const auto start = decoder.decode<QDateTime>(success);
++ QVERIFY(success);
++
++ QCOMPARE(start, QDateTime::fromMSecsSinceEpoch(1691996809123 + index));
++ };
++
++ // Scalar case
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=ServerStatusScalar"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy scalarSpy(node.data(), &QOpcUaNode::attributeRead);
++ node->readValueAttribute();
++ scalarSpy.wait(signalSpyTimeout);
++ QCOMPARE(scalarSpy.size(), 1);
++
++ QCOMPARE(node->valueAttributeError(), QOpcUa::UaStatusCode::Good);
++ checkAndDecodeFirstMember(node->valueAttribute().value<QOpcUaExtensionObject>(), 0);
++
++ // Array case
++ node.reset(opcuaClient->node(u"ns=3;s=ServerStatusArray"_s));
++
++ QSignalSpy arraySpy(node.data(), &QOpcUaNode::attributeRead);
++ node->readValueAttribute();
++ arraySpy.wait(signalSpyTimeout);
++ QCOMPARE(arraySpy.size(), 1);
++
++ QCOMPARE(node->valueAttributeError(), QOpcUa::UaStatusCode::Good);
++ const auto arrayContent = node->valueAttribute().toList();
++ QCOMPARE(arrayContent.size(), 2);
++
++ for (int i = 0; i < arrayContent.size(); ++i) {
++ checkAndDecodeFirstMember(arrayContent.at(i).value<QOpcUaExtensionObject>(), i);
++ }
++
++ // Multi dimensional array case
++ node.reset(opcuaClient->node(u"ns=3;s=ServerStatusMultiDimensionalArray"_s));
++
++ QSignalSpy multiArraySpy(node.data(), &QOpcUaNode::attributeRead);
++ node->readValueAttribute();
++ multiArraySpy.wait(signalSpyTimeout);
++ QCOMPARE(multiArraySpy.size(), 1);
++
++ QCOMPARE(node->valueAttributeError(), QOpcUa::UaStatusCode::Good);
++ const auto multiArray = node->valueAttribute().value<QOpcUaMultiDimensionalArray>();
++ QVERIFY(multiArray.isValid());
++ QCOMPARE(multiArray.arrayDimensions(), QList<quint32>({2, 2}));
++
++ checkAndDecodeFirstMember(multiArray.value({0, 0}).value<QOpcUaExtensionObject>(), 0);
++ checkAndDecodeFirstMember(multiArray.value({0, 1}).value<QOpcUaExtensionObject>(), 1);
++ checkAndDecodeFirstMember(multiArray.value({1, 0}).value<QOpcUaExtensionObject>(), 2);
++ checkAndDecodeFirstMember(multiArray.value({1, 1}).value<QOpcUaExtensionObject>(), 3);
++ }
++
++ void Tst_QOpcUaClient::binaryDataEncodingRecursionDepth()
++ {
++ // DiagnosticInfo with InnerDiagnosticInfo
++ {
++ QOpcUaDiagnosticInfo outerInfo;
++
++ // Recursion depth of 500, decoding should work
++ for (int i = 0; i < 499; ++i) {
++ QOpcUaDiagnosticInfo info;
++ info.setInnerDiagnosticInfo(outerInfo);
++ info.setHasInnerDiagnosticInfo(true);
++ outerInfo = info;
++ }
++
++ {
++ QByteArray data;
++ QOpcUaBinaryDataEncoding codec(&data);
++ bool success = codec.encode(outerInfo);
++ QVERIFY(success);
++
++ const auto decoded = codec.decode<QOpcUaDiagnosticInfo>(success);
++ QVERIFY(success);
++ QCOMPARE(outerInfo, decoded);
++ }
++
++ // New recursion depth of 501, decoding should fail
++ QOpcUaDiagnosticInfo info;
++ info.setInnerDiagnosticInfo(outerInfo);
++ info.setHasInnerDiagnosticInfo(true);
++ outerInfo = info;
++
++ {
++ QByteArray data;
++ QOpcUaBinaryDataEncoding codec(&data);
++ bool success = codec.encode(outerInfo);
++ QVERIFY(success);
++
++ const auto decoded = codec.decode<QOpcUaDiagnosticInfo>(success);
++ QVERIFY(!success);
++ QCOMPARE(decoded, QOpcUaDiagnosticInfo());
++ }
++ }
++
++ // Variant of DataValue of Variant
++ {
++ QOpcUaVariant outerVariant;
++ outerVariant.setValue(QOpcUaVariant::ValueType::Boolean, true);
++
++ // Recursion depth of 500, decoding should work
++ for (int i = 0; i < 249; ++i) {
++ QOpcUaDataValue dv;
++ dv.setValue(outerVariant);
++ outerVariant.setValue(QOpcUaVariant::ValueType::DataValue, dv);
++ }
++
++ {
++ QByteArray data;
++ QOpcUaBinaryDataEncoding codec(&data);
++ bool success = codec.encode(outerVariant);
++ QVERIFY(success);
++
++ const auto decoded = codec.decode<QOpcUaVariant>(success);
++ QVERIFY(success);
++ QCOMPARE(outerVariant, decoded);
++ }
++
++ // New recursion depth of 502, decoding should fail
++ QOpcUaDataValue dv;
++ dv.setValue(outerVariant);
++ outerVariant.setValue(QOpcUaVariant::ValueType::DataValue, dv);
++
++ {
++ QByteArray data;
++ QOpcUaBinaryDataEncoding codec(&data);
++ bool success = codec.encode(outerVariant);
++ QVERIFY(success);
++
++ const auto decoded = codec.decode<QOpcUaVariant>(success);
++ QVERIFY(!success);
++ QCOMPARE(decoded, QOpcUaVariant());
++ }
++ }
++ }
++
++ void Tst_QOpcUaClient::indexRange()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int32"_s));
++ QVERIFY(node != nullptr);
++
++ QVariantList list({0, 1, 2, 3, 4, 5, 6, 7});
++
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Types::Int32);
++
++ QSignalSpy attributeWrittenSpy(node.data(), &QOpcUaNode::attributeWritten);
++
++ node->writeAttributeRange(QOpcUa::NodeAttribute::Value, QVariantList({10, 11, 12, 13}), u"0:3"_s, QOpcUa::Types::Int32);
++ attributeWrittenSpy.wait(signalSpyTimeout);
++ QCOMPARE(attributeWrittenSpy.size(), 1);
++ QCOMPARE(attributeWrittenSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QSignalSpy attributeReadSpy(node.data(), &QOpcUaNode::attributeRead);
++ node->readAttributeRange(QOpcUa::NodeAttribute::Value, u"0:6"_s);
++ attributeReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(attributeReadSpy.size(), 1);
++
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::Value).toList(), QVariantList({10, 11, 12, 13, 4, 5, 6}));
++ }
++
++ void Tst_QOpcUaClient::invalidIndexRange()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int32"_s));
++ QVERIFY(node != nullptr);
++
++ QVariantList list({0, 1, 2, 3, 4, 5, 6, 7});
++
++ WRITE_VALUE_ATTRIBUTE(node, list, QOpcUa::Types::Int32);
++
++ QSignalSpy attributeWrittenSpy(node.data(), &QOpcUaNode::attributeWritten);
++
++ node->writeAttributeRange(QOpcUa::NodeAttribute::Value, QVariantList({10, 11, 12, 13}), u"notavalidrange"_s, QOpcUa::Types::Int32);
++ attributeWrittenSpy.wait(signalSpyTimeout);
++ QCOMPARE(attributeWrittenSpy.size(), 1);
++ QCOMPARE(attributeWrittenSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadIndexRangeInvalid);
++
++ QSignalSpy attributeReadSpy(node.data(), &QOpcUaNode::attributeRead);
++ node->readAttributeRange(QOpcUa::NodeAttribute::Value, u"notavalidrange"_s);
++ attributeReadSpy.wait(signalSpyTimeout);
++ QCOMPARE(attributeReadSpy.size(), 1);
++
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::Value), QOpcUa::UaStatusCode::BadIndexRangeInvalid);
++ }
++
++ void Tst_QOpcUaClient::subscriptionIndexRange()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> integerArrayNode(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.Int32"_s));
++ QVERIFY(integerArrayNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++ p.setIndexRange(u"1"_s);
++ QSignalSpy monitoringEnabledSpy(integerArrayNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(integerArrayNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy writeSpy(integerArrayNode.data(), &QOpcUaNode::attributeWritten);
++ QSignalSpy dataChangeSpy(integerArrayNode.data(), &QOpcUaNode::dataChangeOccurred);
++
++ QVariantList l({0, 1});
++ WRITE_VALUE_ATTRIBUTE(integerArrayNode, l, QOpcUa::Types::Int32);
++ writeSpy.clear();
++
++ integerArrayNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout); // Wait for the initial data change
++ dataChangeSpy.clear();
++ integerArrayNode->writeAttributeRange(QOpcUa::NodeAttribute::Value, 10, u"0"_s, QOpcUa::Types::Int32); // Write the first element of the array
++ writeSpy.wait(signalSpyTimeout);
++ QCOMPARE(writeSpy.size(), 1);
++ QCOMPARE(writeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(writeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ dataChangeSpy.wait(1000);
++ QCOMPARE(dataChangeSpy.size(), 0);
++
++ writeSpy.clear();
++ integerArrayNode->writeAttributeRange(QOpcUa::NodeAttribute::Value, 10, u"1"_s, QOpcUa::Types::Int32); // Write the second element of the array
++ writeSpy.wait(signalSpyTimeout);
++ QCOMPARE(writeSpy.size(), 1);
++ QCOMPARE(writeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(writeSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(integerArrayNode->attribute(QOpcUa::NodeAttribute::Value).toDouble(), 10.0);
++
++ integerArrayNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::subscriptionDataChangeFilter()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> doubleWriteNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleWriteNode != nullptr);
++
++ QScopedPointer<QOpcUaNode> doubleNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++
++ QSignalSpy monitoringEnabledSpy(doubleNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(doubleNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(doubleNode.data(), &QOpcUaNode::dataChangeOccurred);
++ QSignalSpy attributeUpdatedSpy(doubleNode.data(), &QOpcUaNode::attributeUpdated);
++ QSignalSpy monitoringModifiedSpy(doubleNode.data(), &QOpcUaNode::monitoringStatusChanged);
++
++ WRITE_VALUE_ATTRIBUTE(doubleWriteNode, 1.0, QOpcUa::Types::Double);
++
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout); // Wait for the initial data change
++ if (attributeUpdatedSpy.empty())
++ attributeUpdatedSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(attributeUpdatedSpy.size(), 1);
++ dataChangeSpy.clear();
++ attributeUpdatedSpy.clear();
++
++ WRITE_VALUE_ATTRIBUTE(doubleWriteNode, 1.5, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ if (attributeUpdatedSpy.empty())
++ attributeUpdatedSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1); // Data change without filter
++ QCOMPARE(attributeUpdatedSpy.size(), 1);
++ QCOMPARE(doubleNode->attribute(QOpcUa::NodeAttribute::Value), 1.5);
++ dataChangeSpy.clear();
++ attributeUpdatedSpy.clear();
++
++ QOpcUaMonitoringParameters::DataChangeFilter filter;
++ filter.setDeadbandType(QOpcUaMonitoringParameters::DataChangeFilter::DeadbandType::Absolute);
++ filter.setTrigger(QOpcUaMonitoringParameters::DataChangeFilter::DataChangeTrigger::StatusOrValue);
++ filter.setDeadbandValue(1.0);
++ doubleNode->modifyDataChangeFilter(QOpcUa::NodeAttribute::Value, filter);
++ monitoringModifiedSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringModifiedSpy.size(), 1);
++ QCOMPARE(monitoringModifiedSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(monitoringModifiedSpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>() & QOpcUaMonitoringParameters::Parameter::Filter);
++ QCOMPARE(monitoringModifiedSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ WRITE_VALUE_ATTRIBUTE(doubleWriteNode, 2.0, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(1000);
++ if (attributeUpdatedSpy.empty())
++ attributeUpdatedSpy.wait(100);
++ QCOMPARE(dataChangeSpy.size(), 0); // Filter is active and delta is < 1
++ QCOMPARE(attributeUpdatedSpy.size(), 0);
++ attributeUpdatedSpy.clear();
++
++ WRITE_VALUE_ATTRIBUTE(doubleWriteNode, 3.0, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ if (attributeUpdatedSpy.empty())
++ attributeUpdatedSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1); // delta == 1, a data change is expected
++ QCOMPARE(attributeUpdatedSpy.size(), 1);
++ QCOMPARE(doubleNode->attribute(QOpcUa::NodeAttribute::Value), 3.0);
++
++ doubleNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::modifyPublishingMode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> doubleNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++
++ QSignalSpy monitoringEnabledSpy(doubleNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(doubleNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(doubleNode.data(), &QOpcUaNode::dataChangeOccurred);
++ QSignalSpy monitoringStatusSpy(doubleNode.data(), &QOpcUaNode::monitoringStatusChanged);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.0, QOpcUa::Types::Double);
++
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout); // Wait for the initial data change
++ QCOMPARE(dataChangeSpy.size(), 1);
++ dataChangeSpy.clear();
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.5, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ dataChangeSpy.clear();
++
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::PublishingEnabled, false);
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 3.0, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(1000);
++ QCOMPARE(dataChangeSpy.size(), 0);
++
++ doubleNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::modifyMonitoringMode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> doubleNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++
++ QSignalSpy monitoringEnabledSpy(doubleNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(doubleNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(doubleNode.data(), &QOpcUaNode::dataChangeOccurred);
++ QSignalSpy monitoringStatusSpy(doubleNode.data(), &QOpcUaNode::monitoringStatusChanged);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.0, QOpcUa::Types::Double);
++
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout); // Wait for the initial data change
++ QCOMPARE(dataChangeSpy.size(), 1);
++ dataChangeSpy.clear();
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.5, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ dataChangeSpy.clear();
++
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::MonitoringMode,
++ QVariant::fromValue(QOpcUaMonitoringParameters::MonitoringMode::Disabled));
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 3.0, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(1000);
++ QCOMPARE(dataChangeSpy.size(), 0);
++
++ doubleNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::modifyMonitoredItem()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> doubleNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++
++ QSignalSpy monitoringEnabledSpy(doubleNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(doubleNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(doubleNode.data(), &QOpcUaNode::dataChangeOccurred);
++ QSignalSpy monitoringStatusSpy(doubleNode.data(), &QOpcUaNode::monitoringStatusChanged);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.0, QOpcUa::Types::Double);
++
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout); // Wait for the initial data change
++ QCOMPARE(dataChangeSpy.size(), 1);
++ dataChangeSpy.clear();
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 1.5, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(dataChangeSpy.at(0).at(1).value<double>(), 1.5);
++ dataChangeSpy.clear();
++
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::SamplingInterval, 50.0);
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(monitoringStatusSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(monitoringStatusSpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>() & QOpcUaMonitoringParameters::Parameter::SamplingInterval);
++
++ monitoringStatusSpy.clear();
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::DiscardOldest, true);
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(monitoringStatusSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(monitoringStatusSpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>() & QOpcUaMonitoringParameters::Parameter::DiscardOldest);
++
++ monitoringStatusSpy.clear();
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::QueueSize, quint32(42));
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(monitoringStatusSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(monitoringStatusSpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>() & QOpcUaMonitoringParameters::Parameter::QueueSize);
++
++ monitoringStatusSpy.clear();
++ doubleNode->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::DiscardOldest, true);
++ monitoringStatusSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringStatusSpy.size(), 1);
++ QCOMPARE(monitoringStatusSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(monitoringStatusSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(monitoringStatusSpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>() & QOpcUaMonitoringParameters::Parameter::DiscardOldest);
++
++ WRITE_VALUE_ATTRIBUTE(doubleNode, 3.0, QOpcUa::Types::Double);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++
++ doubleNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::addDuplicateMonitoredItem()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> doubleNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.Double"_s));
++ QVERIFY(doubleNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++
++ QSignalSpy monitoringEnabledSpy(doubleNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(doubleNode.data(), &QOpcUaNode::disableMonitoringFinished);
++
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(doubleNode->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ monitoringEnabledSpy.clear();
++ doubleNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadEntryExists);
++ QCOMPARE(doubleNode->monitoringStatus(QOpcUa::NodeAttribute::Value).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ doubleNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::subscriptionUnreadableNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> unreadableNode(opcuaClient->node(u"ns=3;s=VariableWithoutReadPermission"_s));
++ QVERIFY(unreadableNode != nullptr);
++
++ QOpcUaMonitoringParameters p(100);
++ p.setIndexRange(u"1"_s);
++ QSignalSpy monitoringEnabledSpy(unreadableNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(unreadableNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(unreadableNode.data(), &QOpcUaNode::dataChangeOccurred);
++
++ unreadableNode->enableMonitoring(QOpcUa::NodeAttribute::Value, p);
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ // Wait for the initial data change
++ dataChangeSpy.wait(signalSpyTimeout);
++ QCOMPARE(dataChangeSpy.size(), 1);
++ QCOMPARE(dataChangeSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(dataChangeSpy.at(0).at(1), QVariant());
++ QCOMPARE(unreadableNode->valueAttribute(), QVariant());
++ QCOMPARE(unreadableNode->valueAttributeError(), QOpcUa::UaStatusCode::BadUserAccessDenied);
++
++ unreadableNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ }
++
++ void Tst_QOpcUaClient::checkMonitoredItemCleanup()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> readWriteNode(opcuaClient->node(u"ns=3;s=TestNode.ReadWrite"_s));
++ QVERIFY(readWriteNode != nullptr);
++
++ QScopedPointer<QOpcUaNode> serverNode(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server)));
++ QVERIFY(serverNode != nullptr);
++
++ QSignalSpy monitoringEnabledSpy(readWriteNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QOpcUa::NodeAttributes attr = QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::BrowseName;
++ readWriteNode->enableMonitoring(attr, QOpcUaMonitoringParameters(100, QOpcUaMonitoringParameters::SubscriptionType::Exclusive));
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ if (monitoringEnabledSpy.size() != 2)
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 2);
++
++ for (const auto &entry : monitoringEnabledSpy) {
++ QVERIFY(attr & entry.at(0).value<QOpcUa::NodeAttribute>());
++ QCOMPARE(entry.at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ quint32 subscriptionId = readWriteNode->monitoringStatus(QOpcUa::NodeAttribute::Value).subscriptionId();
++ QCOMPARE(subscriptionId, readWriteNode->monitoringStatus(QOpcUa::NodeAttribute::BrowseName).subscriptionId());
++
++ QSignalSpy methodSpy(serverNode.data(), &QOpcUaNode::methodCallFinished);
++ QList<QOpcUa::TypedVariant> parameter;
++ parameter.append(QOpcUa::TypedVariant(QVariant(quint32(subscriptionId)), QOpcUa::Types::UInt32));
++ serverNode->callMethod(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server_GetMonitoredItems), parameter);
++
++ methodSpy.wait(signalSpyTimeout);
++ QCOMPARE(methodSpy.size(), 1);
++
++ for (const auto &entry : methodSpy) {
++ QCOMPARE(entry.at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(entry.at(1).toList().size(), 2); // Two monitored items
++ QCOMPARE(entry.at(1).toList().at(0).toList().size(), 2); // One server handle for each monitored item
++ QCOMPARE(entry.at(1).toList().at(1).toList().size(), 2); // One client handle for each monitored item
++ }
++
++ readWriteNode.reset(); // Delete the node object
++
++ bool success = false;
++ for (int i = 0; i < 15; ++i) {
++ methodSpy.clear();
++ serverNode->callMethod(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server_GetMonitoredItems), parameter);
++ methodSpy.wait(signalSpyTimeout);
++ QCOMPARE(methodSpy.size(), 1);
++
++ if (methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>() != QOpcUa::UaStatusCode::BadSubscriptionIdInvalid) {
++ QTest::qWait(100);
++ continue;
++ }
++
++ QCOMPARE(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::BadSubscriptionIdInvalid);
++ success = true;
++ break;
++ }
++
++ QVERIFY(success);
++ }
++
++ void Tst_QOpcUaClient::checkAttributeUpdated()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=TestNode.ReadWrite"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy spy(node.data(), &QOpcUaNode::attributeUpdated);
++
++ node->readAttributes(QOpcUa::NodeAttribute::Value);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++
++ node->writeValueAttribute(23.0, QOpcUa::Types::Double);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 2);
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100));
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 3);
++
++ for (const auto &it : spy) {
++ QCOMPARE(it.at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QVERIFY(it.at(1).isValid());
++ }
++ }
++
++ void Tst_QOpcUaClient::checkValueAttributeUpdated()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=TestNode.ReadWrite"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy spy(node.data(), &QOpcUaNode::valueAttributeUpdated);
++
++ node->writeValueAttribute(23.0, QOpcUa::Types::Double);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ spy.clear();
++
++ node->readAttributes(QOpcUa::NodeAttribute::Value);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++
++ node->writeValueAttribute(23.0, QOpcUa::Types::Double);
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 2);
++
++ node->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100));
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 3);
++
++ QCOMPARE(spy.at(0).at(0), 23.0);
++ QCOMPARE(spy.at(1).at(0), 23.0);
++ QCOMPARE(spy.at(2).at(0), 23.0);
++ }
++
++ void Tst_QOpcUaClient::checkMonitoringInvalidRequests()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=3;s=TestNode.ReadWrite"_s));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy enableMonitoringSpy(node.get(), &QOpcUaNode::enableMonitoringFinished);
++
++ // Explicit subscription id without existing subscription
++ QOpcUaMonitoringParameters param(200);
++ param.setSubscriptionId(42);
++ node->enableMonitoring(QOpcUa::NodeAttribute::Value, param);
++ enableMonitoringSpy.wait(signalSpyTimeout);
++ QCOMPARE(enableMonitoringSpy.size(), 1);
++ QCOMPARE(enableMonitoringSpy.at(0).at(1), QOpcUa::UaStatusCode::BadSubscriptionIdInvalid);
++
++ // Attempt to modify a monitoring that doesn't exist
++ QSignalSpy monitoringChangedSpy(node.get(), &QOpcUaNode::monitoringStatusChanged);
++ node->modifyMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters::Parameter::QueueSize, 23);
++ monitoringChangedSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringChangedSpy.size(), 1);
++ qDebug() << monitoringChangedSpy;
++ QCOMPARE(monitoringChangedSpy.at(0).at(2), QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
++
++ QSignalSpy disableMonitoringSpy(node.get(), &QOpcUaNode::disableMonitoringFinished);
++ node->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ disableMonitoringSpy.wait(signalSpyTimeout);
++ QCOMPARE(disableMonitoringSpy.size(), 1);
++ QCOMPARE(disableMonitoringSpy.at(0).at(1), QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
++ }
++
++ void Tst_QOpcUaClient::stringCharset()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> stringScalarNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.String"_s));
++ QScopedPointer<QOpcUaNode> localizedScalarNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.LocalizedText"_s));
++ QScopedPointer<QOpcUaNode> stringArrayNode(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.String"_s));
++ QScopedPointer<QOpcUaNode> localizedArrayNode(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.LocalizedText"_s));
++
++ QVERIFY(stringScalarNode != nullptr);
++ QVERIFY(localizedScalarNode != nullptr);
++ QVERIFY(stringArrayNode != nullptr);
++ QVERIFY(localizedArrayNode != nullptr);
++
++ QString testString = u"🞀🞁🞂🞃"_s;
++ QOpcUaLocalizedText lt1(u"en"_s, testString);
++ QOpcUaLocalizedText lt2(u"de"_s, testString);
++
++ WRITE_VALUE_ATTRIBUTE(stringScalarNode, testString, QOpcUa::String);
++ WRITE_VALUE_ATTRIBUTE(localizedScalarNode, localizedTexts[0], QOpcUa::LocalizedText);
++
++ QVariantList l;
++ l.append(testString);
++ l.append(testString);
++
++ WRITE_VALUE_ATTRIBUTE(stringArrayNode, l, QOpcUa::String);
++
++ l.clear();
++ l.append(lt1);
++ l.append(lt2);
++
++ WRITE_VALUE_ATTRIBUTE(localizedArrayNode, l, QOpcUa::LocalizedText);
++
++ READ_MANDATORY_VARIABLE_NODE(stringArrayNode);
++ READ_MANDATORY_VARIABLE_NODE(localizedArrayNode);
++ READ_MANDATORY_VARIABLE_NODE(stringScalarNode);
++ READ_MANDATORY_VARIABLE_NODE(localizedScalarNode);
++
++ QVariant result = stringScalarNode->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(result.toString(), testString);
++ result = localizedScalarNode->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(result.value<QOpcUaLocalizedText>(), localizedTexts[0]);
++
++ result = stringArrayNode->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(result.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(result.toList().size(), 2);
++ QCOMPARE(result.toList()[0].metaType().id(), QMetaType::QString);
++ QCOMPARE(result.toList()[0].toString(), testString);
++ QCOMPARE(result.toList()[1].metaType().id(), QMetaType::QString);
++ QCOMPARE(result.toList()[1].toString(), testString);
++
++ result = localizedArrayNode->attribute(QOpcUa::NodeAttribute::Value);
++ QCOMPARE(result.metaType().id(), QMetaType::QVariantList);
++ QCOMPARE(result.toList().size(), 2);
++ QCOMPARE(result.toList()[0].value<QOpcUaLocalizedText>(), lt1);
++ QCOMPARE(result.toList()[1].value<QOpcUaLocalizedText>(), lt2);
++ }
++
++ void Tst_QOpcUaClient::namespaceArray()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QSignalSpy spy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // Catch the initial update after connect
++ if (spy.empty())
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ spy.clear();
++
++ QCOMPARE(opcuaClient->updateNamespaceArray(), true);
++
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++
++ QStringList namespaces = opcuaClient->namespaceArray();
++ QCOMPARE(namespaces.size(), 5);
++
++ int nsIndex = namespaces.indexOf("http://qt-project.org"_L1);
++ QVERIFY(nsIndex > 0);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::nodeIdFromString(nsIndex, u"Demo.Static.Scalar.String"_s)));
++ READ_MANDATORY_BASE_NODE(node);
++
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>().text(), u"StringScalarTest"_s);
++ }
++
++ void Tst_QOpcUaClient::multiDimensionalArray()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Arrays.MultiDimensionalDouble"_s));
++ QVERIFY(node != nullptr);
++
++ QList<quint32> arrayDimensions({2, 2, 3});
++ QVariantList value({0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0});
++ QOpcUaMultiDimensionalArray arr(value, arrayDimensions);
++ QVERIFY(arr.isValid());
++ WRITE_VALUE_ATTRIBUTE(node, arr , QOpcUa::Double);
++ READ_MANDATORY_VARIABLE_NODE(node);
++
++ QCOMPARE(node->attributeError(QOpcUa::NodeAttribute::Value), QOpcUa::UaStatusCode::Good);
++ QOpcUaMultiDimensionalArray readBack = node->attribute(QOpcUa::NodeAttribute::Value).value<QOpcUaMultiDimensionalArray>();
++
++ QVERIFY(readBack.isValid());
++ QCOMPARE(readBack.value({0, 0, 0}), 0.0);
++ QCOMPARE(readBack.value({0, 0, 1}), 1.0);
++ QCOMPARE(readBack.value({0, 0, 2}), 2.0);
++ QCOMPARE(readBack.value({0, 1, 0}), 3.0);
++ QCOMPARE(readBack.value({0, 1, 1}), 4.0);
++ QCOMPARE(readBack.value({0, 1, 2}), 5.0);
++ QCOMPARE(readBack.value({1, 0, 0}), 6.0);
++ QCOMPARE(readBack.value({1, 0, 1}), 7.0);
++ QCOMPARE(readBack.value({1, 0, 2}), 8.0);
++ QCOMPARE(readBack.value({1, 1, 0}), 9.0);
++ QCOMPARE(readBack.value({1, 1, 1}), 10.0);
++ QCOMPARE(readBack.value({1, 1, 2}), 11.0);
++
++ QCOMPARE(arr, readBack);
++ }
++
++ void Tst_QOpcUaClient::dateTimeConversion()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> dateTimeScalarNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.DateTime"_s));
++
++ QVERIFY(dateTimeScalarNode != nullptr);
++
++ QDateTime dt = QDateTime::currentDateTime();
++
++ WRITE_VALUE_ATTRIBUTE(dateTimeScalarNode, dt, QOpcUa::Types::DateTime);
++ READ_MANDATORY_VARIABLE_NODE(dateTimeScalarNode);
++
++ QDateTime result = dateTimeScalarNode->attribute(QOpcUa::NodeAttribute::Value).toDateTime();
++ QCOMPARE(dt, result);
++ }
++
++ void Tst_QOpcUaClient::timeStamps()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> stringScalarNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.String"_s));
++
++ QVERIFY(stringScalarNode != nullptr);
++
++ QCOMPARE(stringScalarNode->sourceTimestamp(QOpcUa::NodeAttribute::Value).isValid(), false);
++ QCOMPARE(stringScalarNode->serverTimestamp(QOpcUa::NodeAttribute::Value).isValid(), false);
++
++ READ_MANDATORY_VARIABLE_NODE(stringScalarNode);
++
++ const QDateTime sourceRead = stringScalarNode->sourceTimestamp(QOpcUa::NodeAttribute::Value);
++ const QDateTime serverRead = stringScalarNode->serverTimestamp(QOpcUa::NodeAttribute::Value);
++
++ QVERIFY(sourceRead.isValid());
++ QVERIFY(serverRead.isValid());
++
++ QOpcUaMonitoringParameters p(100);
++ QSignalSpy monitoringEnabledSpy(stringScalarNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy monitoringDisabledSpy(stringScalarNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ QSignalSpy dataChangeSpy(stringScalarNode.data(), &QOpcUaNode::dataChangeOccurred);
++
++ QTest::qWait(10); // Make sure the timestamp has a chance to change
++
++ WRITE_VALUE_ATTRIBUTE(stringScalarNode, u"Reset"_s, QOpcUa::Types::String);
++
++ stringScalarNode->enableMonitoring(QOpcUa::NodeAttribute::Value, QOpcUaMonitoringParameters(100));
++
++ monitoringEnabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringEnabledSpy.size(), 1);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringEnabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ dataChangeSpy.wait(signalSpyTimeout);
++ const QDateTime sourceDataChange = stringScalarNode->sourceTimestamp(QOpcUa::NodeAttribute::Value);
++ const QDateTime serverDataChange = stringScalarNode->serverTimestamp(QOpcUa::NodeAttribute::Value);
++
++ QVERIFY(sourceDataChange.isValid());
++ QVERIFY(serverDataChange.isValid());
++ QVERIFY(sourceRead < sourceDataChange);
++ QVERIFY(serverRead < serverDataChange);
++
++ stringScalarNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ monitoringDisabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(monitoringDisabledSpy.size(), 1);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::Value);
++ QCOMPARE(monitoringDisabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::createNodeFromExpandedId()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QSignalSpy updateSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++ opcuaClient->updateNamespaceArray();
++ updateSpy.wait(signalSpyTimeout);
++ QVERIFY(updateSpy.size() > 0);
++ QVERIFY(opcuaClient->namespaceArray().size() > 0);
++
++ // Node on a remote server, nullptr expected
++ QOpcUaExpandedNodeId id;
++ id.setNodeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::RootFolder));
++ id.setServerIndex(1);
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(id));
++ QVERIFY(node == nullptr);
++
++ // Root node on the local server, valid pointer expected
++ id.setServerIndex(0);
++ node.reset(opcuaClient->node(id));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_BASE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(), QOpcUaQualifiedName(0, u"Root"_s));
++
++ // Successful namespace substitution, valid pointer expected
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ node.reset(opcuaClient->node(id));
++ QVERIFY(node != nullptr);
++ QCOMPARE(node->nodeId(), u"ns=3;s=TestNode.ReadWrite"_s);
++ READ_MANDATORY_BASE_NODE(node);
++ QCOMPARE(node->attribute(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>(),
++ QOpcUaQualifiedName(3, u"TestNode.ReadWrite"_s));
++
++ // Invalid namespace, nullptr expected
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"InvalidNamespace"_s);
++ node.reset(opcuaClient->node(id));
++ QVERIFY(node == nullptr);
++ }
++
++ void Tst_QOpcUaClient::checkExpandedIdConversion()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Before the namespace array is populated, error expected
++ bool ok = true;
++ QOpcUaExpandedNodeId id;
++ id.setNodeId(u"ns=0;i=84"_s);
++ id.setNamespaceUri(u"MyNameSpace"_s);
++ QString result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == false);
++ QCOMPARE(result, QString());
++ id.setNamespaceUri(QString());
++
++ QSignalSpy updateSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ if (updateSpy.empty())
++ updateSpy.wait(signalSpyTimeout);
++ QVERIFY(updateSpy.size() > 0);
++ QVERIFY(!opcuaClient->namespaceArray().isEmpty());
++
++ // Root node on the local server, valid string expected
++ id.setServerIndex(0);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == true);
++ QCOMPARE(result, u"ns=0;i=84"_s);
++
++ // Successful namespace substitution, valid string expected
++ ok = false;
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == true);
++ QCOMPARE(result, u"ns=3;s=TestNode.ReadWrite"_s);
++
++ // Invalid namespace, empty string expected
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"InvalidNamespace"_s);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == false);
++ QCOMPARE(result, QString());
++
++ // Malformed node id string, empty string expected
++ ok = true;
++ id.setNodeId(u"ns=0,s=TestNode.ReadWrite"_s);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == false);
++ QCOMPARE(result, QString());
++
++ // Resolving id on different server is expected to fail
++ ok = false;
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ id.setServerIndex(42);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == false);
++
++ // Invalid node id is expected to fail
++ ok = false;
++ id.setNodeId(u"xxxxxxxxxxx"_s);
++ id.setServerIndex(0);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ result = opcuaClient->resolveExpandedNodeId(id, &ok);
++ QVERIFY(ok == false);
++ }
++
++ void Tst_QOpcUaClient::checkExpandedIdConversionNoOk()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Before the namespace array is populated, empty string expected
++ QOpcUaExpandedNodeId id;
++ id.setNodeId(u"ns=0;i=84"_s);
++ id.setNamespaceUri(u"MyNameSpace"_s);
++ QString result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, QString());
++ id.setNamespaceUri(QString());
++
++ QSignalSpy updateSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ if (updateSpy.empty())
++ updateSpy.wait(signalSpyTimeout);
++ QVERIFY(updateSpy.size() > 0);
++ QVERIFY(!opcuaClient->namespaceArray().isEmpty());
++
++ // Root node on the local server, valid string expected
++ id.setServerIndex(0);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, u"ns=0;i=84"_s);
++
++ // Successful namespace substitution, valid string expected
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, u"ns=3;s=TestNode.ReadWrite"_s);
++
++ // Invalid namespace, empty string expected
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"InvalidNamespace"_s);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, QString());
++
++ // Malformed node id string, empty string expected
++ id.setNodeId(u"ns=0,s=TestNode.ReadWrite"_s);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, QString());
++
++ // Resolving id on different server is expected to fail
++ id.setNodeId(u"ns=0;s=TestNode.ReadWrite"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ id.setServerIndex(42);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, QString());
++
++ // Invalid node id is expected to fail
++ id.setNodeId(u"xxxxxxxxxxx"_s);
++ id.setNamespaceUri(u"Test Namespace"_s);
++ id.setServerIndex(0);
++ result = opcuaClient->resolveExpandedNodeId(id);
++ QCOMPARE(result, QString());
++ }
++
++ void Tst_QOpcUaClient::createQualifiedName()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Before the namespace array is populated, empty qualified name expected
++ bool ok = true;
++ QOpcUaQualifiedName name = opcuaClient->qualifiedNameFromNamespaceUri(u"Test Namespace"_s, u"Name"_s, &ok);
++ QVERIFY(ok == false);
++ QCOMPARE(name, QOpcUaQualifiedName());
++
++ QSignalSpy updateSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ if (updateSpy.empty())
++ updateSpy.wait(signalSpyTimeout);
++ QVERIFY(updateSpy.size() > 0);
++ QVERIFY(!opcuaClient->namespaceArray().isEmpty());
++
++ // Valid namespace, valid qualified name expected
++ name = opcuaClient->qualifiedNameFromNamespaceUri(u"Test Namespace"_s, u"Name"_s, &ok);
++ QVERIFY(ok == true);
++ QCOMPARE(name, QOpcUaQualifiedName(3, u"Name"_s));
++
++ // Invalid namespace, empty qualified name expected
++ name = opcuaClient->qualifiedNameFromNamespaceUri(u"InvalidNamespace"_s, u"Name"_s, &ok);
++ QVERIFY(ok == false);
++ QCOMPARE(name, QOpcUaQualifiedName());
++ }
++
++ void Tst_QOpcUaClient::createQualifiedNameNoOk()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Before the namespace array is populated, empty qualified name expected
++ QOpcUaQualifiedName name = opcuaClient->qualifiedNameFromNamespaceUri(u"Test Namespace"_s, u"Name"_s);
++ QCOMPARE(name, QOpcUaQualifiedName());
++
++ QSignalSpy updateSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ if (updateSpy.empty())
++ updateSpy.wait(signalSpyTimeout);
++ QVERIFY(updateSpy.size() > 0);
++ QVERIFY(!opcuaClient->namespaceArray().isEmpty());
++
++ // Valid namespace, valid qualified name expected
++ name = opcuaClient->qualifiedNameFromNamespaceUri(u"Test Namespace"_s, u"Name"_s);
++ QCOMPARE(name, QOpcUaQualifiedName(3, u"Name"_s));
++
++ // Invalid namespace, empty qualified name expected
++ name = opcuaClient->qualifiedNameFromNamespaceUri(u"InvalidNamespace"_s, u"Name"_s);
++ QCOMPARE(name, QOpcUaQualifiedName());
++ }
++
++ void Tst_QOpcUaClient::resolveBrowsePath()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> typesNode(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::TypesFolder)));
++ QVERIFY(typesNode != nullptr);
++
++ QSignalSpy spy(typesNode.data(), &QOpcUaNode::resolveBrowsePathFinished);
++
++ QList<QOpcUaRelativePathElement> path;
++ const QString referenceTypeId = QOpcUa::nodeIdFromReferenceType(QOpcUa::ReferenceTypeId::Organizes);
++ path.append(QOpcUaRelativePathElement(QOpcUaQualifiedName(0, u"DataTypes"_s), referenceTypeId));
++ path.append(QOpcUaRelativePathElement(QOpcUaQualifiedName(0, u"BaseDataType"_s), referenceTypeId));
++ bool success = typesNode->resolveBrowsePath(path);
++ QVERIFY(success == true);
++
++ spy.wait(signalSpyTimeout);
++ QCOMPARE(spy.size(), 1);
++ const auto results = spy.at(0).at(0).value<QList<QOpcUaBrowsePathTarget>>();
++ QCOMPARE(results.size(), 1);
++ QCOMPARE(results.at(0).remainingPathIndex(), (std::numeric_limits<quint32>::max)());
++ QCOMPARE(results.at(0).targetId().nodeId(), QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseDataType));
++ QVERIFY(results.at(0).targetId().namespaceUri().isEmpty());
++ QCOMPARE(results.at(0).isFullyResolved(), true);
++ QCOMPARE(results.at(0).targetId().serverIndex(), 0U);
++ QCOMPARE(spy.at(0).at(1).value<QList<QOpcUaRelativePathElement>>(), path);
++ QCOMPARE(spy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::extensionObjectWithGuid()
++ {
++ const QByteArray uuidWireData = QByteArray::fromHex("f827ce6cbeb61f48a5a888fd2bbc4fb7");
++ const QByteArray opcuaWireData = QByteArray::fromHex("040400") + uuidWireData;
++ const QString uuidId = u"6cce27f8-b6be-481f-a5a8-88fd2bbc4fb7"_s;
++ const QString sampleNodeId = u"ns=4;g="_s + uuidId;
++
++ const auto uuid = QUuid::fromString(uuidId);
++ QCOMPARE(uuid.toString(QUuid::WithoutBraces), uuidId);
++
++ QOpcUaExtensionObject obj;
++ obj.setEncoding(QOpcUaExtensionObject::Encoding::ByteString);
++ obj.setEncodingTypeId(u"ns=2;s=MyEncoding1"_s);
++ QOpcUaBinaryDataEncoding encoder(obj);
++ encoder.encode<QString, QOpcUa::Types::NodeId>(sampleNodeId);
++ QCOMPARE(obj.encodedBody().toHex(), opcuaWireData.toHex());
++
++ QByteArray buffer = obj.encodedBody();
++ QOpcUaBinaryDataEncoding decoder(&buffer);
++
++ bool success = false;
++ const auto decodedNodeId = decoder.decode<QString, QOpcUa::Types::NodeId>(success);
++ QVERIFY(success);
++ QCOMPARE(decodedNodeId, sampleNodeId);
++ }
++
++ void Tst_QOpcUaClient::encodeEmptyStringNodeId()
++ {
++ QByteArray data;
++ QOpcUaBinaryDataEncoding encoder(&data);
++
++ auto success = encoder.encode<QString, QOpcUa::Types::NodeId>(QString());
++ QCOMPARE(success, true);
++
++ const auto result = encoder.decode<QString, QOpcUa::Types::NodeId>(success);
++ QCOMPARE(success, true);
++ QCOMPARE(result, u"ns=0;i=0"_s);
++ }
++
++ void Tst_QOpcUaClient::statusStrings()
++ {
++ QCOMPARE(statusToString(QOpcUa::Good), u"Good"_s);
++ QCOMPARE(statusToString(QOpcUa::BadAggregateConfigurationRejected), u"BadAggregateConfigurationRejected"_s);
++ }
++
++ void Tst_QOpcUaClient::addNamespace()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ QSignalSpy namespaceUpdatedSpy(opcuaClient, &QOpcUaClient::namespaceArrayUpdated);
++ QSignalSpy namespaceChangedSpy(opcuaClient, &QOpcUaClient::namespaceArrayChanged);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ opcuaClient->updateNamespaceArray();
++ namespaceUpdatedSpy.wait(signalSpyTimeout);
++ if (namespaceUpdatedSpy.size() != 2)
++ namespaceUpdatedSpy.wait(signalSpyTimeout);
++ QVERIFY(namespaceUpdatedSpy.size() > 0);
++ QCOMPARE(namespaceChangedSpy.size(), 1);
++
++ // Update second time: No change signal emitted
++ namespaceChangedSpy.clear();
++ namespaceUpdatedSpy.clear();
++ opcuaClient->updateNamespaceArray();
++ namespaceUpdatedSpy.wait(signalSpyTimeout);
++ // The signal is emitted roughly at the same time
++ // as namespaceArrayUpdated()
++ namespaceChangedSpy.wait(100);
++
++ QCOMPARE(namespaceUpdatedSpy.size(), 1);
++ QCOMPARE(namespaceChangedSpy.size(), 0);
++
++ auto namespaceArray = opcuaClient->namespaceArray();
++ QString newNamespaceName = u"DynamicTestNamespace#%1"_s.arg(namespaceArray.size());
++
++ QVERIFY(!namespaceArray.isEmpty());
++ QVERIFY(!namespaceArray.contains(newNamespaceName));
++
++ QList<QOpcUa::TypedVariant> args;
++ args.push_back(QOpcUa::TypedVariant(newNamespaceName, QOpcUa::String));
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server)));
++ QVERIFY(node != nullptr);
++
++ QSignalSpy methodSpy(node.data(), &QOpcUaNode::methodCallFinished);
++
++ opcuaClient->setNamespaceAutoupdate(true);
++ namespaceUpdatedSpy.clear();
++ namespaceChangedSpy.clear();
++
++ bool success = node->callMethod(u"ns=3;s=Test.Method.AddNamespace"_s, args);
++ QVERIFY(success == true);
++
++ methodSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(methodSpy.size(), 1);
++ QCOMPARE(methodSpy.at(0).at(0).value<QString>(), u"ns=3;s=Test.Method.AddNamespace"_s);
++ QCOMPARE(methodSpy.at(0).at(1).value<quint16>(), namespaceArray.size());
++ QCOMPARE(QOpcUa::isSuccessStatus(methodSpy.at(0).at(2).value<QOpcUa::UaStatusCode>()), true);
++
++ // Do not call updateNamespaceArray()
++ if (namespaceChangedSpy.isEmpty())
++ namespaceChangedSpy.wait(signalSpyTimeout);
++
++ QVERIFY(namespaceUpdatedSpy.size() > 0);
++ QCOMPARE(namespaceChangedSpy.size(), 1);
++ auto updatedNamespaceArray = opcuaClient->namespaceArray();
++ QCOMPARE(updatedNamespaceArray.size(), namespaceArray.size() + 1);
++ QVERIFY(updatedNamespaceArray.contains(newNamespaceName));
++ QCOMPARE(methodSpy.at(0).at(1).value<quint16>(), updatedNamespaceArray.indexOf(newNamespaceName));
++ }
++
++ void Tst_QOpcUaClient::fixedTimestamp()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.FixedTimestamp"_s));
++ QVERIFY(node != nullptr);
++ READ_MANDATORY_VARIABLE_NODE(node);
++ QVariant value = node->attribute(QOpcUa::NodeAttribute::Value);
++ QVERIFY(value.isValid());
++ QCOMPARE(value.metaType().id(), QMetaType::QDateTime);
++ QCOMPARE(value.toDateTime(), QDateTime(QDate(2012, 12, 19), QTime(13, 37)));
++ }
++
++ void Tst_QOpcUaClient::eventSubscription()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> serverNode(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server)));
++ QVERIFY(serverNode != nullptr);
++
++ QScopedPointer<QOpcUaNode> serverNode2(opcuaClient->node(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Server)));
++ QVERIFY(serverNode2 != nullptr);
++
++ QScopedPointer<QOpcUaNode> testFolderNode(opcuaClient->node(u"ns=3;s=TestFolder"_s));
++ QVERIFY(testFolderNode != nullptr);
++
++ QSignalSpy enabledSpy(serverNode.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy enabledSpy2(serverNode2.data(), &QOpcUaNode::enableMonitoringFinished);
++ QSignalSpy eventSpy(serverNode.data(), &QOpcUaNode::eventOccurred);
++ QSignalSpy eventSpy2(serverNode2.data(), &QOpcUaNode::eventOccurred);
++
++ QOpcUaMonitoringParameters::EventFilter filter;
++ filter << QOpcUaSimpleAttributeOperand(u"Severity"_s);
++ filter << QOpcUaSimpleAttributeOperand(u"Message"_s);
++
++ QOpcUaMonitoringParameters p(0);
++ p.setQueueSize(10); // Without setting the queue size, we get a queue overflow event after triggering both events without waiting
++ p.setFilter(filter);
++
++ serverNode->enableMonitoring(QOpcUa::NodeAttribute::EventNotifier, p);
++ enabledSpy.wait();
++ QCOMPARE(enabledSpy.size(), 1);
++ QCOMPARE(enabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::EventNotifier);
++ QCOMPARE(enabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(serverNode->monitoringStatus(QOpcUa::NodeAttribute::EventNotifier).filter().value<QOpcUaMonitoringParameters::EventFilter>(),
++ filter);
++
++ // Add a second monitoring with where clause
++ QOpcUaMonitoringParameters::EventFilter filterWithWhereClause = filter;
++
++ // Only events with severity >= 700
++ QOpcUaContentFilterElement whereElement;
++ whereElement << QOpcUaContentFilterElement::FilterOperator::GreaterThanOrEqual << QOpcUaSimpleAttributeOperand(u"Severity"_s) <<
++ QOpcUaLiteralOperand(quint16(700), QOpcUa::Types::UInt16);
++ filterWithWhereClause << whereElement;
++ p.setFilter(filterWithWhereClause);
++
++ serverNode2->enableMonitoring(QOpcUa::NodeAttribute::EventNotifier, p);
++ enabledSpy2.wait();
++ QCOMPARE(enabledSpy2.size(), 1);
++ QCOMPARE(enabledSpy2.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::EventNotifier);
++ QCOMPARE(enabledSpy2.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ QCOMPARE(serverNode2->monitoringStatus(QOpcUa::NodeAttribute::EventNotifier).filter().value<QOpcUaMonitoringParameters::EventFilter>(),
++ filterWithWhereClause);
++
++ // Disabled because the open62541 server does not currently return an EventFilterResult
++ // QVERIFY(serverNode->monitoringStatus(QOpcUa::NodeAttribute::EventNotifier).filterResult().isValid());
++ // QOpcUaEventFilterResult res = serverNode->monitoringStatus(QOpcUa::NodeAttribute::EventNotifier).filterResult().value<QOpcUaEventFilterResult>();
++ // QVERIFY(res.isGood() == true);
++
++ testFolderNode->callMethod(u"ns=2;s=TriggerEvent"_s, { QOpcUa::TypedVariant(750, QOpcUa::Types::UInt16) }); // Trigger event over the severity threshold
++ testFolderNode->callMethod(u"ns=2;s=TriggerEvent"_s, { QOpcUa::TypedVariant(699, QOpcUa::Types::UInt16) }); // Trigger event below the severity threshold
++
++ eventSpy.wait();
++ if (eventSpy.size() != 2)
++ eventSpy.wait();
++
++ if (eventSpy2.size() < 1)
++ eventSpy2.wait();
++ if (eventSpy2.size() < 1)
++ eventSpy2.wait();
++
++ QCOMPARE(eventSpy.size(), 2);
++ QCOMPARE(eventSpy.at(0).at(0).toList().size(), 2);
++
++ const auto expectedMessage = QOpcUaLocalizedText(u"en"_s, u"An event has been generated"_s);
++
++ QCOMPARE(eventSpy.at(0).at(0).toList().at(0).value<quint16>(), 750);
++ QCOMPARE(eventSpy.at(0).at(0).toList().at(1).value<QOpcUaLocalizedText>(), expectedMessage);
++
++ QCOMPARE(eventSpy.at(1).at(0).toList().size(), 2);
++ QCOMPARE(eventSpy.at(1).at(0).toList().at(0).value<quint16>(), 699);
++ QCOMPARE(eventSpy.at(1).at(0).toList().at(1).value<QOpcUaLocalizedText>(), expectedMessage);
++
++ QCOMPARE(eventSpy2.size(), 1);
++ QCOMPARE(eventSpy2.at(0).at(0).toList().size(), 2);
++ QCOMPARE(eventSpy2.at(0).at(0).toList().at(0).value<quint16>(), 750);
++ QCOMPARE(eventSpy2.at(0).at(0).toList().at(1).value<QOpcUaLocalizedText>(), expectedMessage);
++
++ eventSpy.clear();
++
++ QSignalSpy modifySpy(serverNode.data(), &QOpcUaNode::monitoringStatusChanged);
++ filter << QOpcUaSimpleAttributeOperand(u"SourceNode"_s);
++ serverNode->modifyEventFilter(filter);
++ modifySpy.wait();
++ QCOMPARE(modifySpy.size(), 1);
++ QCOMPARE(modifySpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::EventNotifier);
++ QVERIFY(modifySpy.at(0).at(1).value<QOpcUaMonitoringParameters::Parameters>().testFlag(QOpcUaMonitoringParameters::Parameter::Filter) == true);
++ QCOMPARE(modifySpy.at(0).at(2).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(serverNode->monitoringStatus(QOpcUa::NodeAttribute::EventNotifier).filter().value<QOpcUaMonitoringParameters::EventFilter>(),
++ filter);
++
++ testFolderNode->callMethod(u"ns=2;s=TriggerEvent"_s, { QOpcUa::TypedVariant(800, QOpcUa::Types::UInt16) }); // Trigger event
++ eventSpy.wait();
++ QCOMPARE(eventSpy.size(), 1);
++ QCOMPARE(eventSpy.at(0).at(0).toList().size(), 3);
++ QCOMPARE(eventSpy.at(0).at(0).toList().at(0).value<quint16>(), 800);
++ QCOMPARE(eventSpy.at(0).at(0).toList().at(1).value<QOpcUaLocalizedText>(), expectedMessage);
++ QCOMPARE(eventSpy.at(0).at(0).toList().at(2).toString(), u"ns=0;i=2253"_s);
++
++ QSignalSpy disabledSpy(serverNode.data(), &QOpcUaNode::disableMonitoringFinished);
++ serverNode->disableMonitoring(QOpcUa::NodeAttribute::EventNotifier);
++ disabledSpy.wait();
++ QCOMPARE(disabledSpy.size(), 1);
++ QCOMPARE(disabledSpy.at(0).at(0).value<QOpcUa::NodeAttribute>(), QOpcUa::NodeAttribute::EventNotifier);
++ QCOMPARE(disabledSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++
++ disabledSpy.clear();
++ serverNode->disableMonitoring(QOpcUa::NodeAttribute::Value);
++ disabledSpy.wait(signalSpyTimeout);
++ QCOMPARE(disabledSpy.size(), 1);
++ QCOMPARE(disabledSpy.at(0).at(1), QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
++ }
++
++ void Tst_QOpcUaClient::readHistoryDataFromNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Historizing1"_s));
++ QVERIFY (node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, 0, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 1, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 2, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 3, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 4, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 5, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 6, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 7, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 8, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 9, QOpcUa::Types::Int32);
++
++ QScopedPointer<QOpcUaNode> nodeWithLimit(opcuaClient->node(u"ns=2;s=Demo.Static.Historizing1.ContinuationPoint"_s));
++ QVERIFY (nodeWithLimit != nullptr);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 0, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 1, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 2, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 3, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 4, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 5, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 6, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 7, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 8, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 9, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++
++ // Due to extensive optimizations, using currentDateTime() without adding 1 ms sometimes doesn't get
++ // all values because of the 100 ns resolution of the OPC UA timestamp used by the server.
++
++ // All available data in chronological order
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime::currentDateTime(),
++ QDateTime::currentDateTime().addDays(-1),
++ 15, false, QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++ QCOMPARE(response->hasMoreData(), false);
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 3);
++ QCOMPARE(result[0].result()[7].value(), 2);
++ QCOMPARE(result[0].result()[8].value(), 1);
++ QCOMPARE(result[0].result()[9].value(), 0);
++ }
++
++ // All available data in chronological order with continuation point
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(nodeWithLimit->readHistoryRaw(QDateTime::currentDateTime(),
++ QDateTime::currentDateTime().addDays(-1),
++ 15, false, QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Reading);
++
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++ QSignalSpy responseStateSpy(response.get(), &QOpcUaHistoryReadResponse::stateChanged);
++ responseStateSpy.wait(signalSpyTimeout);
++ if (readHistoryDataSpy.empty())
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(responseStateSpy.size(), 1);
++ QCOMPARE(responseStateSpy.at(0).at(0).value<QOpcUaHistoryReadResponse::State>(),
++ QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ responseStateSpy.clear();
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 5);
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++
++ readHistoryDataSpy.clear();
++ QCOMPARE(response->hasMoreData(), true);
++ response->readMoreData();
++ responseStateSpy.wait(signalSpyTimeout);
++ if (readHistoryDataSpy.empty())
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(responseStateSpy.size(), 1);
++ QCOMPARE(responseStateSpy.at(0).at(0).value<QOpcUaHistoryReadResponse::State>(),
++ QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 3);
++ QCOMPARE(result[0].result()[7].value(), 2);
++ QCOMPARE(result[0].result()[8].value(), 1);
++ QCOMPARE(result[0].result()[9].value(), 0);
++
++ readHistoryDataSpy.clear();
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++
++ // All available data in reverse order
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime::currentDateTime().addDays(-1),
++ QDateTime::currentDateTime(), 15, false,
++ QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 0);
++ QCOMPARE(result[0].result()[1].value(), 1);
++ QCOMPARE(result[0].result()[2].value(), 2);
++ QCOMPARE(result[0].result()[3].value(), 3);
++ QCOMPARE(result[0].result()[4].value(), 4);
++ QCOMPARE(result[0].result()[5].value(), 5);
++ QCOMPARE(result[0].result()[6].value(), 6);
++ QCOMPARE(result[0].result()[7].value(), 7);
++ QCOMPARE(result[0].result()[8].value(), 8);
++ QCOMPARE(result[0].result()[9].value(), 9);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++ // Empty data set for a time range without data
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime::currentDateTime().addDays(-3),
++ QDateTime::currentDateTime().addDays(-2),
++ 5, false, QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto resultNoData = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(resultNoData.size(), 1);
++ QCOMPARE(resultNoData[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(resultNoData[0].count(), 0);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++ // Only a starting time
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime::currentDateTime().addDays(-1),
++ QDateTime(), 15, false, QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 0);
++ QCOMPARE(result[0].result()[1].value(), 1);
++ QCOMPARE(result[0].result()[2].value(), 2);
++ QCOMPARE(result[0].result()[3].value(), 3);
++ QCOMPARE(result[0].result()[4].value(), 4);
++ QCOMPARE(result[0].result()[5].value(), 5);
++ QCOMPARE(result[0].result()[6].value(), 6);
++ QCOMPARE(result[0].result()[7].value(), 7);
++ QCOMPARE(result[0].result()[8].value(), 8);
++ QCOMPARE(result[0].result()[9].value(), 9);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++
++ // Only an end time
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime(),
++ QDateTime::currentDateTime().addDays(1), 15, false,
++ QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 3);
++ QCOMPARE(result[0].result()[7].value(), 2);
++ QCOMPARE(result[0].result()[8].value(), 1);
++ QCOMPARE(result[0].result()[9].value(), 0);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++ // Return bounds
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryRaw(QDateTime::currentDateTime().addDays(-1),
++ QDateTime(), 15, true,
++ QOpcUa::TimestampsToReturn::Server));
++
++ QVERIFY(response != nullptr);
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ const auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 1);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 12);
++ QCOMPARE(result[0].result()[0].statusCode(), QOpcUa::UaStatusCode::BadBoundNotFound);
++ QCOMPARE(result[0].result()[0].value(), QVariant());
++ QCOMPARE(result[0].result()[1].value(), 0);
++ QCOMPARE(result[0].result()[2].value(), 1);
++ QCOMPARE(result[0].result()[3].value(), 2);
++ QCOMPARE(result[0].result()[4].value(), 3);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 5);
++ QCOMPARE(result[0].result()[7].value(), 6);
++ QCOMPARE(result[0].result()[8].value(), 7);
++ QCOMPARE(result[0].result()[9].value(), 8);
++ QCOMPARE(result[0].result()[10].value(), 9);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++ }
++
++ void Tst_QOpcUaClient::readHistoryDataFromClient()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=Demo.Static.Historizing2"_s));
++ QVERIFY (node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, 0, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 1, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 2, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 3, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 4, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 5, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 6, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 7, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 8, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(node, 9, QOpcUa::Types::Int32);
++
++ QScopedPointer<QOpcUaNode> nodeWithLimit(opcuaClient->node(u"ns=2;s=Demo.Static.Historizing2.ContinuationPoint"_s));
++ QVERIFY (nodeWithLimit != nullptr);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 10, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 11, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 12, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 13, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 14, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 15, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 16, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 17, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 18, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 19, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++
++ // Values for two nodes in chronological order
++ // One of the nodes will require two calls to get all values
++ {
++ QOpcUaHistoryReadRawRequest request(
++ {QOpcUaReadItem(node->nodeId()), QOpcUaReadItem(nodeWithLimit->nodeId())},
++ QDateTime::currentDateTime(),
++ QDateTime::currentDateTime().addDays(-1),
++ 15,
++ false);
++ request.setTimestampsToReturn(QOpcUa::TimestampsToReturn::Server);
++
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryData(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ auto result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 2);
++ QCOMPARE(result[0].nodeId(), u"ns=2;s=Demo.Static.Historizing2"_s);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 3);
++ QCOMPARE(result[0].result()[7].value(), 2);
++ QCOMPARE(result[0].result()[8].value(), 1);
++ QCOMPARE(result[0].result()[9].value(), 0);
++ QCOMPARE(result[1].count(), 5);
++ QCOMPARE(result[1].nodeId(), u"ns=2;s=Demo.Static.Historizing2.ContinuationPoint"_s);
++ QCOMPARE(result[1].result()[0].value(), 19);
++ QCOMPARE(result[1].result()[1].value(), 18);
++ QCOMPARE(result[1].result()[2].value(), 17);
++ QCOMPARE(result[1].result()[3].value(), 16);
++ QCOMPARE(result[1].result()[4].value(), 15);
++
++ QCOMPARE(response->hasMoreData(), true);
++ readHistoryDataSpy.clear();
++
++ response->readMoreData();
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ result = readHistoryDataSpy.at(0).at(0).value<QList<QOpcUaHistoryData>>();
++ QCOMPARE(result.size(), 2);
++ QCOMPARE(result[0].nodeId(), u"ns=2;s=Demo.Static.Historizing2"_s);
++ QCOMPARE(result[0].statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(result[0].count(), 10);
++ QCOMPARE(result[0].result()[0].value(), 9);
++ QCOMPARE(result[0].result()[1].value(), 8);
++ QCOMPARE(result[0].result()[2].value(), 7);
++ QCOMPARE(result[0].result()[3].value(), 6);
++ QCOMPARE(result[0].result()[4].value(), 5);
++ QCOMPARE(result[0].result()[5].value(), 4);
++ QCOMPARE(result[0].result()[6].value(), 3);
++ QCOMPARE(result[0].result()[7].value(), 2);
++ QCOMPARE(result[0].result()[8].value(), 1);
++ QCOMPARE(result[0].result()[9].value(), 0);
++ QCOMPARE(result[1].count(), 10);
++ QCOMPARE(result[1].nodeId(), u"ns=2;s=Demo.Static.Historizing2.ContinuationPoint"_s);
++ QCOMPARE(result[1].result()[0].value(), 19);
++ QCOMPARE(result[1].result()[1].value(), 18);
++ QCOMPARE(result[1].result()[2].value(), 17);
++ QCOMPARE(result[1].result()[3].value(), 16);
++ QCOMPARE(result[1].result()[4].value(), 15);
++ QCOMPARE(result[1].result()[5].value(), 14);
++ QCOMPARE(result[1].result()[6].value(), 13);
++ QCOMPARE(result[1].result()[7].value(), 12);
++ QCOMPARE(result[1].result()[8].value(), 11);
++ QCOMPARE(result[1].result()[9].value(), 10);
++
++ QCOMPARE(response->hasMoreData(), false);
++ }
++
++ // Don't follow the continuation points to the end and release remaining
++ {
++ WRITE_VALUE_ATTRIBUTE(nodeWithLimit, 20, QOpcUa::Types::Int32);
++ QTest::qWait(1);
++
++ QOpcUaHistoryReadRawRequest request(
++ {QOpcUaReadItem(node->nodeId()), QOpcUaReadItem(nodeWithLimit->nodeId())},
++ QDateTime::currentDateTime(),
++ QDateTime::currentDateTime().addDays(-1),
++ 15,
++ false);
++ request.setTimestampsToReturn(QOpcUa::TimestampsToReturn::Server);
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryData(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy readHistoryDataSpy(response.get(), &QOpcUaHistoryReadResponse::readHistoryDataFinished);
++ readHistoryDataSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++ QCOMPARE(readHistoryDataSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(response->hasMoreData(), true);
++ readHistoryDataSpy.clear();
++ response->readMoreData();
++ readHistoryDataSpy.wait(signalSpyTimeout);
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++ QCOMPARE(response->hasMoreData(), true);
++
++ const auto data = response.data();
++
++ response->releaseContinuationPoints();
++ readHistoryDataSpy.clear();
++ readHistoryDataSpy.wait(signalSpyTimeout);
++ QCOMPARE(readHistoryDataSpy.size(), 1);
++ QCOMPARE(readHistoryDataSpy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(response.data(), data);
++ }
++ }
++
++ void Tst_QOpcUaClient::readHistoryEventsFromNode()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=2;s=EventHistorian"_s));
++ QVERIFY(node != nullptr);
++
++ QOpcUaMonitoringParameters::EventFilter filter;
++ filter << QOpcUaSimpleAttributeOperand(u"Message"_s);
++ filter << QOpcUaSimpleAttributeOperand(u"Time"_s);
++
++ const auto firstAvailableEventTime = QDateTime::fromMSecsSinceEpoch(1694153836000 - 120 * 60 * 1000);
++ const auto lastAvailableEventTime = QDateTime::fromMSecsSinceEpoch(1694153836000);
++
++ // Read all at once
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryEvents(firstAvailableEventTime, lastAvailableEventTime, filter, 10));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(!response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(response->events().size(), 1);
++ QCOMPARE(response->events().at(0).events().size(), 3);
++ QCOMPARE(response->events().at(0).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(0).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(0).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(0).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ QCOMPARE(response->events().at(0).events().at(2).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(2).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 3"_s));
++ QCOMPARE(response->events().at(0).events().at(2).at(1), QDateTime::fromMSecsSinceEpoch(1694153836000));
++
++ QCOMPARE(spy.at(0).at(0).value<QList<QOpcUaHistoryEvent>>(), response->events());
++ }
++
++ // Test continuation points
++ {
++ QScopedPointer<QOpcUaHistoryReadResponse> response(node->readHistoryEvents(firstAvailableEventTime, lastAvailableEventTime, filter, 1));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ spy.clear();
++ response->readMoreData();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ spy.clear();
++ response->readMoreData();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(!response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(response->events().size(), 1);
++ QCOMPARE(response->events().at(0).events().size(), 3);
++ QCOMPARE(response->events().at(0).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(0).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(0).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(0).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ QCOMPARE(response->events().at(0).events().at(2).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(2).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 3"_s));
++ QCOMPARE(response->events().at(0).events().at(2).at(1), QDateTime::fromMSecsSinceEpoch(1694153836000));
++ }
++ }
++
++ void Tst_QOpcUaClient::readHistoryEventsFromClient()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QOpcUaMonitoringParameters::EventFilter filter;
++ filter << QOpcUaSimpleAttributeOperand(u"Message"_s);
++ filter << QOpcUaSimpleAttributeOperand(u"Time"_s);
++
++ const auto firstAvailableEventTime = QDateTime::fromMSecsSinceEpoch(1694153836000 - 120 * 60 * 1000);
++ const auto lastAvailableEventTime = QDateTime::fromMSecsSinceEpoch(1694153836000);
++
++ // Read all at once
++ {
++ QOpcUaHistoryReadEventRequest request({ QOpcUaReadItem(u"ns=2;s=EventHistorian"_s), QOpcUaReadItem(u"ns=2;s=EventHistorian2"_s) },
++ firstAvailableEventTime, lastAvailableEventTime, filter);
++ request.setNumValuesPerNode(10);
++
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryEvents(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(!response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(response->events().size(), 2);
++ QCOMPARE(response->events().at(0).events().size(), 3);
++ QCOMPARE(response->events().at(0).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(0).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(0).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(0).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ QCOMPARE(response->events().at(0).events().at(2).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(2).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 3"_s));
++ QCOMPARE(response->events().at(0).events().at(2).at(1), QDateTime::fromMSecsSinceEpoch(1694153836000));
++
++ QCOMPARE(response->events().at(1).events().size(), 2);
++ QCOMPARE(response->events().at(1).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(1).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(1).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(1).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(1).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(1).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++
++ QCOMPARE(spy.at(0).at(0).value<QList<QOpcUaHistoryEvent>>(), response->events());
++ }
++
++ // Test continuation points
++ {
++ QOpcUaHistoryReadEventRequest request({ QOpcUaReadItem(u"ns=2;s=EventHistorian"_s), QOpcUaReadItem(u"ns=2;s=EventHistorian2"_s) },
++ firstAvailableEventTime, lastAvailableEventTime, filter);
++ request.setNumValuesPerNode(1);
++
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryEvents(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ spy.clear();
++ response->readMoreData();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ spy.clear();
++ response->readMoreData();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(!response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(response->events().size(), 2);
++ QCOMPARE(response->events().at(0).events().size(), 3);
++ QCOMPARE(response->events().at(0).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(0).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(0).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(0).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ QCOMPARE(response->events().at(0).events().at(2).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(2).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 3"_s));
++ QCOMPARE(response->events().at(0).events().at(2).at(1), QDateTime::fromMSecsSinceEpoch(1694153836000));
++
++ QCOMPARE(response->events().at(1).events().size(), 2);
++ QCOMPARE(response->events().at(1).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(1).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(1).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(1).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(1).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(1).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ }
++
++ // Test continuation points release
++ {
++ QOpcUaHistoryReadEventRequest request({ QOpcUaReadItem(u"ns=2;s=EventHistorian"_s), QOpcUaReadItem(u"ns=2;s=EventHistorian2"_s) },
++ firstAvailableEventTime, lastAvailableEventTime, filter);
++ request.setNumValuesPerNode(1);
++
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryEvents(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++ spy.clear();
++ response->readMoreData();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::MoreDataAvailable);
++
++ const auto events = response->events();
++
++ response->releaseContinuationPoints();
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++ spy.clear();
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(response->events(), events);
++ }
++
++ // Check invalid node id
++ {
++ QOpcUaHistoryReadEventRequest request({ QOpcUaReadItem(u"ns=2;s=EventHistorian"_s), QOpcUaReadItem(u"ns=2;s=EventHistorian3"_s) },
++ firstAvailableEventTime, lastAvailableEventTime, filter);
++ request.setNumValuesPerNode(10);
++ QScopedPointer<QOpcUaHistoryReadResponse> response(opcuaClient->readHistoryEvents(request));
++ QVERIFY(response != nullptr);
++
++ QSignalSpy spy(response.get(), &QOpcUaHistoryReadResponse::readHistoryEventsFinished);
++ spy.wait();
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++
++ QVERIFY(!response->hasMoreData());
++ QCOMPARE(response->state(), QOpcUaHistoryReadResponse::State::Finished);
++
++ QCOMPARE(response->events().size(), 2);
++ QCOMPARE(response->events().at(0).events().size(), 3);
++ QCOMPARE(response->events().at(0).events().at(0).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(0).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 1"_s));
++ QCOMPARE(response->events().at(0).events().at(0).at(1), QDateTime::fromMSecsSinceEpoch(1694146636000));
++ QCOMPARE(response->events().at(0).events().at(1).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(1).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 2"_s));
++ QCOMPARE(response->events().at(0).events().at(1).at(1), QDateTime::fromMSecsSinceEpoch(1694150236000));
++ QCOMPARE(response->events().at(0).events().at(2).size(), 2);
++ QCOMPARE(response->events().at(0).events().at(2).at(0), QOpcUaLocalizedText(u"en"_s, u"Message 3"_s));
++ QCOMPARE(response->events().at(0).events().at(2).at(1), QDateTime::fromMSecsSinceEpoch(1694153836000));
++
++ QCOMPARE(response->events().at(1).events().size(), 0);
++ QCOMPARE(response->events().at(1).statusCode(), QOpcUa::UaStatusCode::BadNodeIdInvalid);
++ }
++ }
++
++ void Tst_QOpcUaClient::decodeGenericStruct()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++
++ // Initialization should fail if the client is not connected
++ QOpcUaGenericStructHandler decoder(opcuaClient);
++
++ auto success = decoder.initialize();
++ QCOMPARE(success, false);
++
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ // After the connection was established, initialization should work
++ QSignalSpy spy(&decoder, &QOpcUaGenericStructHandler::initializedChanged);
++
++ success = decoder.initialize();
++ QCOMPARE(success, true);
++
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).toBool(), true);
++
++ const auto testEnumerationTypeId = u"ns=4;i=3002"_s;
++ const auto testStructTypeId = u"ns=4;i=3003"_s;
++ const auto testStructEncodingId = u"ns=4;i=5001"_s;
++ const auto abstractBaseStructTypeId = u"ns=4;i=3008"_s;
++
++ QCOMPARE(decoder.typeNameForTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::StatusCode)), u"StatusCode"_s);
++ QCOMPARE(decoder.typeNameForTypeId(testEnumerationTypeId), u"QtTestEnumeration"_s);
++ QCOMPARE(decoder.typeNameForTypeId(testStructTypeId), u"QtTestStructType"_s);
++ QCOMPARE(decoder.typeNameForTypeId(abstractBaseStructTypeId), u"QtAbstractBaseStruct"_s);
++ QCOMPARE(decoder.typeNameForTypeId(u"ns=10;i=1234"_s), QString());
++
++ QCOMPARE(decoder.dataTypeKindForTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::StatusCode)),
++ QOpcUaGenericStructHandler::DataTypeKind::Other);
++ QCOMPARE(decoder.dataTypeKindForTypeId(testEnumerationTypeId), QOpcUaGenericStructHandler::DataTypeKind::Enum);
++ QCOMPARE(decoder.dataTypeKindForTypeId(testStructTypeId), QOpcUaGenericStructHandler::DataTypeKind::Struct);
++ QCOMPARE(decoder.dataTypeKindForTypeId(u"ns=10;i=1234"_s), QOpcUaGenericStructHandler::DataTypeKind::Unknown);
++
++ QCOMPARE(decoder.isAbstractTypeId(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Number)), true);
++ QCOMPARE(decoder.isAbstractTypeId(testEnumerationTypeId), false);
++ QCOMPARE(decoder.isAbstractTypeId(testStructTypeId), false);
++ QCOMPARE(decoder.isAbstractTypeId(abstractBaseStructTypeId), true);
++
++ QCOMPARE(decoder.structureDefinitionForBinaryEncodingId(testStructEncodingId).structureType(),
++ QOpcUaStructureDefinition::StructureType::Structure);
++
++ QCOMPARE(decoder.typeNameForBinaryEncodingId(decoder.structureDefinitionForBinaryEncodingId(testStructEncodingId).defaultEncodingId()),
++ u"QtTestStructType"_s);
++
++ QOpcUaReadItem readNestedStruct(u"ns=4;i=6009"_s);
++ QOpcUaReadItem readUnion1(u"ns=4;i=6011"_s);
++ QOpcUaReadItem readUnion2(u"ns=4;i=6003"_s);
++ QOpcUaReadItem readStructWithOptField(u"ns=4;i=6010"_s);
++ QOpcUaReadItem readStructWithoutOptField(u"ns=4;i=6002"_s);
++ QOpcUaReadItem readStructWithDiagnosticInfo(u"ns=4;i=6006"_s);
++ QOpcUaReadItem readStructWithDataValue(u"ns=4;i=6027"_s);
++
++ QSignalSpy readSpy(opcuaClient, &QOpcUaClient::readNodeAttributesFinished);
++
++ const auto readDispatch = opcuaClient->readNodeAttributes({readNestedStruct, readUnion1, readUnion2,
++ readStructWithOptField, readStructWithoutOptField,
++ readStructWithDiagnosticInfo, readStructWithDataValue});
++ QCOMPARE(readDispatch, true);
++
++ readSpy.wait();
++
++ QCOMPARE(readSpy.size(), 1);
++ QCOMPARE(readSpy.at(0).size(), 2);
++ QCOMPARE(readSpy.at(0).at(1).value<QOpcUa::UaStatusCode>(), QOpcUa::UaStatusCode::Good);
++ auto results = readSpy.at(0).at(0).value<QList<QOpcUaReadResult>>();
++
++ QCOMPARE(results.size(), 7);
++ QCOMPARE(results.at(0).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(1).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(2).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(3).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(4).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(5).statusCode(), QOpcUa::UaStatusCode::Good);
++ QCOMPARE(results.at(6).statusCode(), QOpcUa::UaStatusCode::Good);
++
++ {
++ auto nestedStructExt = results.at(0).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(nestedStructExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QCOMPARE(decodedData->typeName(), u"QtTestStructType"_s);
++
++ QCOMPARE(decodedData->fields().value(u"EnumMember"_s).toInt(), 1);
++ QCOMPARE(decodedData->fields().value(u"Int64ArrayMember"_s).value<QList<qint64>>(),
++ QList<qint64>({std::numeric_limits<qint64>::max(),
++ std::numeric_limits<qint64>::max() - 1,
++ std::numeric_limits<qint64>::min()}));
++
++ QCOMPARE(decodedData->fields().value(u"LocalizedTextMember"_s).value<QOpcUaLocalizedText>().locale(), u"en"_s);
++ QCOMPARE(decodedData->fields().value(u"LocalizedTextMember"_s).value<QOpcUaLocalizedText>().text(), u"TestText"_s);
++
++ QCOMPARE(decodedData->fields().value(u"QualifiedNameMember"_s).value<QOpcUaQualifiedName>().namespaceIndex(), 1);
++ QCOMPARE(decodedData->fields().value(u"QualifiedNameMember"_s).value<QOpcUaQualifiedName>().name(), u"TestName"_s);
++
++ QCOMPARE(decodedData->fields().value(u"StringMember"_s).value<QString>(), u"TestString"_s);
++
++ auto nestedStructValue = decodedData->fields().value(u"NestedStructMember"_s).value<QOpcUaGenericStructValue>();
++ QCOMPARE(nestedStructValue.typeName(), u"QtInnerTestStructType"_s);
++ QCOMPARE(nestedStructValue.fields().value(u"DoubleSubtypeMember"_s).toDouble(), 42.0);
++
++ auto nestedStructArrayValue = decodedData->fields().value(u"NestedStructArrayMember"_s).value<QList<QOpcUaGenericStructValue>>();
++ QCOMPARE(nestedStructArrayValue.size(), 2);
++ QCOMPARE(nestedStructArrayValue.at(0).typeName(), u"QtInnerTestStructType"_s);
++ QCOMPARE(nestedStructArrayValue.at(0).fields().value(u"DoubleSubtypeMember"_s).toDouble(), 23.0);
++ QCOMPARE(nestedStructArrayValue.at(1).typeName(), u"QtInnerTestStructType"_s);
++ QCOMPARE(nestedStructArrayValue.at(1).fields().value(u"DoubleSubtypeMember"_s).toDouble(), 42.0);
++ }
++
++ {
++ auto unionExt = results.at(1).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(unionExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QCOMPARE(decodedData->typeName(), u"QtTestUnionType"_s);
++ QCOMPARE(decodedData->fields().value(u"Member1"_s), 42);
++ }
++
++ {
++ auto unionExt = results.at(2).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(unionExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QCOMPARE(decodedData->typeName(), u"QtTestUnionType"_s);
++
++ auto unionFieldValue = decodedData->fields().value(u"Member2"_s).value<QOpcUaGenericStructValue>();
++ QCOMPARE(unionFieldValue.fields().value(u"DoubleSubtypeMember"_s).toDouble(), 23.0);
++ }
++
++ // Struct type with optional field with optional field set
++ {
++ auto optionalFieldExt = results.at(3).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(optionalFieldExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QCOMPARE(decodedData->typeName(), u"QtStructWithOptionalFieldType"_s);
++
++ QCOMPARE(decodedData->fields().value(u"MandatoryMember"_s).toDouble(), 42.0);
++ QCOMPARE(decodedData->fields().value(u"OptionalMember"_s).toDouble(), 23.0);
++ }
++
++ // Struct type with optional field without optional field set
++ {
++ auto optionalFieldExt = results.at(4).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(optionalFieldExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QCOMPARE(decodedData->typeName(), u"QtStructWithOptionalFieldType"_s);
++
++ QCOMPARE(decodedData->fields().value(u"MandatoryMember"_s).toDouble(), 42.0);
++ QCOMPARE(decodedData->fields().contains(u"OptionalMember"_s), false);
++ }
++
++ {
++ auto diagnosticInfoExt = results.at(5).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(diagnosticInfoExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++ QCOMPARE(decodedData->fields().contains(u"DiagnosticInfoMember"_s), true);
++ QCOMPARE(decodedData->fields().value(u"DiagnosticInfoMember"_s).value<QOpcUaDiagnosticInfo>(), testDiagnosticInfos.at(0));
++ QCOMPARE(decodedData->fields().contains(u"DiagnosticInfoArrayMember"_s), true);
++ QCOMPARE(decodedData->fields().value(u"DiagnosticInfoArrayMember"_s).value<QList<QOpcUaDiagnosticInfo>>(), testDiagnosticInfos);
++ }
++
++ {
++ auto dataValueExt = results.at(6).value().value<QOpcUaExtensionObject>();
++ auto decodedData = decoder.decode(dataValueExt);
++ QVERIFY(decodedData);
++ qDebug() << *decodedData;
++
++ QVERIFY(decodedData->fields().contains(u"DataValueMember"_s));
++ const auto dv = decodedData->fields().value(u"DataValueMember"_s).value<QOpcUaDataValue>();
++ QCOMPARE(dv.statusCode(), QOpcUa::UaStatusCode::BadInternalError);
++ QCOMPARE(dv.serverTimestamp(), QDateTime::fromMSecsSinceEpoch(1698655307000));
++ QCOMPARE(dv.sourceTimestamp(), QDateTime::fromMSecsSinceEpoch(1698655306000));
++ QCOMPARE(dv.sourcePicoseconds(), 42);
++ QCOMPARE(dv.serverPicoseconds(), 23);
++ const auto v = dv.value().value<QOpcUaVariant>();
++ QCOMPARE(v.value(), (QList<QString>{u"TestString 1"_s, u"TestString 2"_s, u"TestString 3"_s, u"TestString 4"_s}));
++ QCOMPARE(v.arrayDimensions(), (QList<qint32>{ 2, 2 }));
++
++ QVERIFY(decodedData->fields().contains(u"VariantMember"_s));
++ const auto var = decodedData->fields().value(u"VariantMember"_s).value<QOpcUaVariant>();
++ QVERIFY(var.arrayDimensions().isEmpty());
++ QCOMPARE(var.type(), QOpcUaVariant::ValueType::UInt64);
++ QCOMPARE(var.value(), 42);
++ }
++ }
++
++ void Tst_QOpcUaClient::encodeGenericStruct()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QOpcUaGenericStructHandler handler(opcuaClient);
++
++ QSignalSpy spy(&handler, &QOpcUaGenericStructHandler::initializedChanged);
++
++ auto success = handler.initialize();
++ QCOMPARE(success, true);
++
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).toBool(), true);
++
++ {
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3003"_s);
++ value.fieldsRef()[u"StringMember"_s] = u"StringMember"_s;
++ value.fieldsRef()[u"LocalizedTextMember"_s] = QOpcUaLocalizedText(u"en"_s, u"Localized text"_s);
++ value.fieldsRef()[u"QualifiedNameMember"_s] = QOpcUaQualifiedName(1, u"Qualified name"_s);
++ value.fieldsRef()[u"Int64ArrayMember"_s] = QVariant::fromValue(QList<qint64>({1, 2, 3, 4}));
++ value.fieldsRef()[u"EnumMember"_s] = 42;
++
++ auto innerValue = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue.fieldsRef()[u"DoubleSubtypeMember"_s] = 42.0;
++
++ value.fieldsRef()[u"NestedStructMember"_s] = innerValue;
++
++ auto innerValue1 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue1.fieldsRef()[u"DoubleSubtypeMember"_s] = 23.0;
++
++ auto innerValue2 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue2.fieldsRef()[u"DoubleSubtypeMember"_s] = 42.0;
++
++ value.fieldsRef()[u"NestedStructArrayMember"_s] =
++ QVariant::fromValue(QList<QOpcUaGenericStructValue>({innerValue1, innerValue2}));
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++
++ const auto decoded = handler.decode(*ext);
++ QVERIFY(decoded);
++
++ QCOMPARE(decoded->fields().value(u"StringMember"_s), u"StringMember"_s);
++ QCOMPARE(decoded->fields().value(u"LocalizedTextMember"_s), QOpcUaLocalizedText(u"en"_s, u"Localized text"_s));
++ QCOMPARE(decoded->fields().value(u"QualifiedNameMember"_s), QOpcUaQualifiedName(1, u"Qualified name"_s));
++ QCOMPARE(decoded->fields().value(u"Int64ArrayMember"_s), QVariant::fromValue(QList<qint64>({1, 2, 3, 4})));
++ QCOMPARE(decoded->fields().value(u"EnumMember"_s), 42);
++
++ const auto innerDecoded = decoded->fields().value(u"NestedStructMember"_s).value<QOpcUaGenericStructValue>();
++ QCOMPARE(innerDecoded.fields().value(u"DoubleSubtypeMember"_s), 42.0);
++
++ auto nestedStructArrayValue = decoded->fields().value(u"NestedStructArrayMember"_s).value<QList<QOpcUaGenericStructValue>>();
++ QCOMPARE(nestedStructArrayValue.size(), 2);
++ QCOMPARE(nestedStructArrayValue.at(0).typeName(), u"QtInnerTestStructType"_s);
++ QCOMPARE(nestedStructArrayValue.at(0).fields().value(u"DoubleSubtypeMember"_s).toDouble(), 23.0);
++ QCOMPARE(nestedStructArrayValue.at(1).typeName(), u"QtInnerTestStructType"_s);
++ QCOMPARE(nestedStructArrayValue.at(1).fields().value(u"DoubleSubtypeMember"_s).toDouble(), 42.0);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6009"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto unionValue = handler.createGenericStructValueForTypeId(u"ns=4;i=3005"_s);
++ unionValue.fieldsRef()[u"Member1"_s] = 23;
++
++ auto ext = handler.encode(unionValue);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), unionValue.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++ QVERIFY(decoded);
++
++ QCOMPARE(decoded->fields().value(u"Member1"_s), 23);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6011"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto unionValue = handler.createGenericStructValueForTypeId(u"ns=4;i=3005"_s);
++ auto innerValue = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue.fieldsRef()[u"DoubleSubtypeMember"_s] = 42.0;
++ unionValue.fieldsRef()[u"Member2"_s] = innerValue;
++
++ auto ext = handler.encode(unionValue);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), unionValue.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++ QVERIFY(decoded);
++
++ QVERIFY(decoded->fields().contains(u"Member2"_s));
++
++ const auto innerDecoded = decoded->fields().value(u"Member2"_s).value<QOpcUaGenericStructValue>();
++ QCOMPARE(innerDecoded.fields().value(u"DoubleSubtypeMember"_s), 42.0);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6003"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3006"_s);
++ value.fieldsRef()[u"MandatoryMember"_s] = 23.0;
++ value.fieldsRef()[u"OptionalMember"_s] = 42.0;
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++
++ QCOMPARE(decoded->fields().value(u"MandatoryMember"_s), 23.0);
++ QCOMPARE(decoded->fields().value(u"OptionalMember"_s), 42.0);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6010"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3006"_s);
++ value.fieldsRef()[u"MandatoryMember"_s] = 23.0;
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++
++ QCOMPARE(decoded->fields().value(u"MandatoryMember"_s), 23.0);
++ QCOMPARE(decoded->fields().contains(u"OptionalMember"_s), false);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6002"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3009"_s);
++ value.fieldsRef()[u"DiagnosticInfoMember"_s] = testDiagnosticInfos.at(0);
++ value.fieldsRef()[u"DiagnosticInfoArrayMember"_s] = QVariant::fromValue(testDiagnosticInfos);
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++ QCOMPARE(decoded->fields().value(u"DiagnosticInfoMember"_s), testDiagnosticInfos.at(0));
++ QCOMPARE(decoded->fields().value(u"DiagnosticInfoArrayMember"_s).value<QList<QOpcUaDiagnosticInfo>>(), testDiagnosticInfos);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6006"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3010"_s);
++ value.fieldsRef()[u"DataValueMember"_s] = testDataValues.at(0);
++ value.fieldsRef()[u"VariantMember"_s] = testVariants.at(0);
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++
++ QCOMPARE(decoded->fields().value(u"DataValueMember"_s), testDataValues.at(0));
++ QCOMPARE(decoded->fields().value(u"VariantMember"_s), testVariants.at(0));
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6027"_s));
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++
++ {
++ auto innermost = handler.createGenericStructValueForTypeId(u"ns=4;i=3012"_s);
++ innermost.fieldsRef()[u"StringMember"_s] = u"Custom innermost string"_s;
++ innermost.fieldsRef()[u"RecursiveArrayMember"_s] = QVariant::fromValue(QList<QOpcUaGenericStructValue>());
++
++ auto inner = handler.createGenericStructValueForTypeId(u"ns=4;i=3012"_s);
++ inner.fieldsRef()[u"StringMember"_s] = u"Custom inner string"_s;
++ inner.fieldsRef()[u"RecursiveArrayMember"_s] = QVariant::fromValue(QList<QOpcUaGenericStructValue>{ innermost });
++
++ auto value = handler.createGenericStructValueForTypeId(u"ns=4;i=3012"_s);
++ value.fieldsRef()[u"StringMember"_s] = u"Custom outer string"_s;
++ value.fieldsRef()[u"RecursiveArrayMember"_s] = QVariant::fromValue(QList<QOpcUaGenericStructValue>{ inner });
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++
++ QCOMPARE(decoded->fields().value(u"StringMember"_s), u"Custom outer string"_s);
++ const auto innerDecoded = decoded->fields().value(u"RecursiveArrayMember"_s).value<QList<QOpcUaGenericStructValue>>();
++ QCOMPARE(innerDecoded.size(), 1);
++ QCOMPARE(innerDecoded.at(0).fields().value(u"StringMember"_s), u"Custom inner string"_s);
++ const auto innermostDecoded = innerDecoded.at(0).fields().value(u"RecursiveArrayMember"_s).value<QList<QOpcUaGenericStructValue>>();
++ QCOMPARE(innermostDecoded.size(), 1);
++ QCOMPARE(innermostDecoded.at(0).fields().value(u"StringMember"_s), u"Custom innermost string"_s);
++
++ QScopedPointer<QOpcUaNode> node(opcuaClient->node(u"ns=4;i=6029"_s));
++ QVERIFY(node != nullptr);
++ WRITE_VALUE_ATTRIBUTE(node, *ext, QOpcUa::Types::ExtensionObject);
++ }
++ }
++
++ void Tst_QOpcUaClient::encodeCustomGenericStruct()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QOpcUaGenericStructHandler handler(opcuaClient);
++
++ QSignalSpy spy(&handler, &QOpcUaGenericStructHandler::initializedChanged);
++
++ auto success = handler.initialize();
++ QCOMPARE(success, true);
++
++ spy.wait();
++
++ QCOMPARE(spy.size(), 1);
++ QCOMPARE(spy.at(0).at(0).toBool(), true);
++
++ {
++ QOpcUaStructureDefinition def;
++ def.setStructureType(QOpcUaStructureDefinition::StructureType::Structure);
++ def.setBaseDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseDataType));
++ def.setDefaultEncodingId(u"ns=5;i=42"_s);
++
++ QOpcUaStructureField field1;
++ field1.setArrayDimensions({2, 2});
++ field1.setValueRank(2);
++ field1.setDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::Int32));
++ field1.setIsOptional(false);
++ field1.setName(u"MatrixMember"_s);
++
++ def.setFields({field1});
++
++ auto success = handler.addCustomStructureDefinition(def, u"ns=5;i=23"_s, u"MyMatrixTestStruct"_s);
++ QVERIFY(success);
++
++ auto value = handler.createGenericStructValueForTypeId(u"ns=5;i=23"_s);
++ value.fieldsRef()[u"MatrixMember"_s] = QOpcUaMultiDimensionalArray({1, 2, 3, 4}, {2, 2});
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++ QVERIFY(success);
++
++ const auto matrix = decoded->fields().value(u"MatrixMember"_s).value<QOpcUaMultiDimensionalArray>();
++ QCOMPARE(matrix.arrayDimensions(), QList<quint32>({2, 2}));
++ QCOMPARE(matrix.value(QList<quint32>({0, 0})), 1);
++ QCOMPARE(matrix.value(QList<quint32>({0, 1})), 2);
++ QCOMPARE(matrix.value(QList<quint32>({1, 0})), 3);
++ QCOMPARE(matrix.value(QList<quint32>({1, 1})), 4);
++ }
++
++ {
++ QOpcUaStructureDefinition def;
++ def.setStructureType(QOpcUaStructureDefinition::StructureType::Structure);
++ def.setBaseDataType(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::BaseDataType));
++ def.setDefaultEncodingId(u"ns=5;i=43"_s);
++
++ QOpcUaStructureField field1;
++ field1.setArrayDimensions({2, 2});
++ field1.setValueRank(2);
++ field1.setDataType(u"ns=4;i=3004"_s);
++ field1.setIsOptional(false);
++ field1.setName(u"StructMatrixMember"_s);
++
++ def.setFields({field1});
++
++ auto success = handler.addCustomStructureDefinition(def, u"ns=5;i=24"_s, u"MyStructMatrixTestStruct"_s);
++ QVERIFY(success);
++
++ auto value = handler.createGenericStructValueForTypeId(u"ns=5;i=24"_s);
++
++ auto innerValue1 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue1.fieldsRef()[u"DoubleSubtypeMember"_s] = 1;
++ auto innerValue2 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue2.fieldsRef()[u"DoubleSubtypeMember"_s] = 2;
++ auto innerValue3 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue3.fieldsRef()[u"DoubleSubtypeMember"_s] = 3;
++ auto innerValue4 = handler.createGenericStructValueForTypeId(u"ns=4;i=3004"_s);
++ innerValue4.fieldsRef()[u"DoubleSubtypeMember"_s] = 4;
++
++ value.fieldsRef()[u"StructMatrixMember"_s] =
++ QOpcUaMultiDimensionalArray(QVariantList({innerValue1, innerValue2, innerValue3, innerValue4}), {2, 2});
++
++ auto ext = handler.encode(value);
++ QVERIFY(ext);
++ QCOMPARE(ext->encodingTypeId(), value.structureDefinition().defaultEncodingId());
++
++ const auto decoded = handler.decode(*ext);
++ QVERIFY(decoded);
++
++ const auto matrix = decoded->fields().value(u"StructMatrixMember"_s).value<QOpcUaMultiDimensionalArray>();
++ QCOMPARE(matrix.arrayDimensions(), QList<quint32>({2, 2}));
++ QCOMPARE(matrix.value(QList<quint32>({0, 0})).value<QOpcUaGenericStructValue>().fields().value(u"DoubleSubtypeMember"_s), 1);
++ QCOMPARE(matrix.value(QList<quint32>({0, 1})).value<QOpcUaGenericStructValue>().fields().value(u"DoubleSubtypeMember"_s), 2);
++ QCOMPARE(matrix.value(QList<quint32>({1, 0})).value<QOpcUaGenericStructValue>().fields().value(u"DoubleSubtypeMember"_s), 3);
++ QCOMPARE(matrix.value(QList<quint32>({1, 1})).value<QOpcUaGenericStructValue>().fields().value(u"DoubleSubtypeMember"_s), 4);
++ }
++ }
++
++ void Tst_QOpcUaClient::registerUnregisterNodes()
++ {
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ const QStringList nodesToRegister { u"ns=2;s=Demo.Static.Scalar.Boolean"_s, u"ns=2;s=Demo.Static.Scalar.Int32"_s };
++
++ QSignalSpy registerNodesSpy(opcuaClient, &QOpcUaClient::registerNodesFinished);
++ opcuaClient->registerNodes(nodesToRegister);
++ registerNodesSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(registerNodesSpy.size(), 1);
++ QCOMPARE(registerNodesSpy.at(0).at(0), nodesToRegister);
++ const auto registeredIds = registerNodesSpy.at(0).at(1).value<QStringList>();
++ QCOMPARE(registeredIds.size(), 2);
++ QCOMPARE(registerNodesSpy.at(0).at(2), QOpcUa::UaStatusCode::Good);
++
++ QScopedPointer<QOpcUaNode> node1(opcuaClient->node(registeredIds.at(0)));
++ QScopedPointer<QOpcUaNode> node2(opcuaClient->node(registeredIds.at(1)));
++ QVERIFY(node1 != nullptr);
++ QVERIFY(node2 != nullptr);
++
++ READ_MANDATORY_BASE_NODE(node1);
++ READ_MANDATORY_BASE_NODE(node2);
++
++ QCOMPARE(node1->attribute(QOpcUa::NodeAttribute::BrowseName), QOpcUaQualifiedName(2, u"BoolScalarTest"_s));
++ QCOMPARE(node2->attribute(QOpcUa::NodeAttribute::BrowseName), QOpcUaQualifiedName(2, u"Int32ScalarTest"_s));
++
++ QSignalSpy unregisterNodesSpy(opcuaClient, &QOpcUaClient::unregisterNodesFinished);
++ opcuaClient->unregisterNodes(registeredIds);
++ unregisterNodesSpy.wait(signalSpyTimeout);
++
++ QCOMPARE(unregisterNodesSpy.size(), 1);
++ QCOMPARE(unregisterNodesSpy.at(0).at(0), registeredIds);
++ QCOMPARE(unregisterNodesSpy.at(0).at(1), QOpcUa::UaStatusCode::Good);
++ }
++
++ void Tst_QOpcUaClient::connectionLost()
++ {
++ // Restart the test server if necessary
++ if (m_serverProcess.state() != QProcess::ProcessState::Running) {
++ m_serverProcess.start(m_testServerPath);
++ QVERIFY2(m_serverProcess.waitForStarted(), qPrintable(m_serverProcess.errorString()));
++ QTest::qSleep(2000);
++ }
++
++ QFETCH(QOpcUaClient *, opcuaClient);
++ OpcuaConnector connector(opcuaClient, m_endpoint);
++
++ QCOMPARE(opcuaClient->state(), QOpcUaClient::ClientState::Connected);
++
++ QScopedPointer<QOpcUaNode> stringNode(opcuaClient->node(u"ns=2;s=Demo.Static.Scalar.String"_s));
++
++ QSignalSpy stateSpy(opcuaClient, &QOpcUaClient::stateChanged);
++ QSignalSpy readSpy(stringNode.data(), &QOpcUaNode::attributeRead);
++
++ m_serverProcess.kill();
++ m_serverProcess.waitForFinished();
++ QCOMPARE(m_serverProcess.state(), QProcess::ProcessState::NotRunning);
++
++ // Read twice to speed up broken pipe detection for the asynchronous requests
++ stringNode->readAttributes(QOpcUa::NodeAttribute::BrowseName);
++ stringNode->readAttributes(QOpcUa::NodeAttribute::BrowseName);
++
++ readSpy.wait(signalSpyTimeout);
++ if (stateSpy.size() != 2)
++ stateSpy.wait(15000); // open62541 uses a timeout of 5 seconds for service calls, better be safe.
++ QCOMPARE(readSpy.size(), 2);
++ QVERIFY(readSpy.at(0).at(0).value<QOpcUa::NodeAttributes>() & QOpcUa::NodeAttribute::BrowseName);
++
++ const auto statusCode = stringNode->attributeError(QOpcUa::NodeAttribute::BrowseName);
++ const auto metaEnum = QMetaEnum::fromType<QOpcUa::UaStatusCode>();
++ const auto stringCandidate = metaEnum.valueToKey(statusCode);
++ const auto statusCodeString = u"Unexpected status code: %1"_s.arg(
++ stringCandidate ? QString::fromUtf8(stringCandidate)
++ : QString::number(statusCode, 16));
++
++ // open62541 returns a different status code depending on when after the disconnect the request is made
++ QVERIFY2(statusCode == QOpcUa::UaStatusCode::BadInternalError ||
++ statusCode == QOpcUa::UaStatusCode::BadSecureChannelClosed ||
++ statusCode == QOpcUa::UaStatusCode::BadSessionClosed ||
++ statusCode == QOpcUa::UaStatusCode::BadConnectionClosed ||
++ statusCode == QOpcUa::UaStatusCode::BadServerNotConnected ||
++ statusCode == QOpcUa::UaStatusCode::BadDisconnect,
++ qUtf8Printable(statusCodeString));
++
++ QCOMPARE(stateSpy.size(), 1);
++ QCOMPARE(stateSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::ClientState::Disconnected);
++ }
++
++ void Tst_QOpcUaClient::cleanupTestCase()
++ {
++ if (m_serverProcess.state() == QProcess::Running) {
++ m_serverProcess.kill();
++ m_serverProcess.waitForFinished(2000);
++ }
++ }
++
++ void Tst_QOpcUaClient::loadInvalidPlugin()
++ {
++ QOpcUaProvider provider;
++ QScopedPointer<QOpcUaClient> client(provider.createClient(u"doesnotexist"_s));
++ QCOMPARE(client, nullptr);
++ }
++
++ int main(int argc, char *argv[])
++ {
++ populateDataTypeDefinitionTestData();
++ populateDiagnosticInfoTestData();
++ populateVariantTestData();
++ populateDataValuesTestData();
++ populateEventFilterTestData();
++
++ QCoreApplication app(argc, argv);
++
++ QTEST_SET_MAIN_SOURCE_PATH
++
++ // run tests for all available backends
++ QStringList availableBackends = QOpcUaProvider::availableBackends();
++ if (availableBackends.empty()) {
++ qDebug("No OPCUA backends found, skipping tests.");
++ return EXIT_SUCCESS;
++ }
++
++ Tst_QOpcUaClient tc;
++ return QTest::qExec(&tc, argc, argv);
++ }
++
++ #include "tst_client.moc"
++