From 3c898b6fb4cb1f08e664a9703d3981d2e7b3a0f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 19:06:05 +0000 Subject: [PATCH 1/4] Add backwards compatibility for legacy XML project files (data levels 3-5) Reintroduce read-only support for the old XML-based .mbs project file format. The XML parser converts legacy files into the same ProjectSettings structure used by the JSON parser, including constructing the adapter settings JSON blob from XML connection/device elements. Saving always produces JSON v6, effectively auto-migrating old files. All XML-specific code is isolated in separate files (ProjectFileXmlParser) for easy future removal. The only change to existing code is a small format-detection branch in ProjectFileHandler::openProjectFile(). https://claude.ai/code/session_01Lyu9U25MtH34zG7gUFLe2u --- src/importexport/projectfiledefinitions.h | 18 + src/importexport/projectfilehandler.cpp | 19 +- src/importexport/projectfilexmlparser.cpp | 800 ++++++++++++++++++ src/importexport/projectfilexmlparser.h | 65 ++ tests/importexport/CMakeLists.txt | 1 + tests/importexport/projectfilexmltestdata.cpp | 277 ++++++ tests/importexport/projectfilexmltestdata.h | 24 + .../importexport/tst_projectfilexmlparser.cpp | 277 ++++++ tests/importexport/tst_projectfilexmlparser.h | 33 + 9 files changed, 1512 insertions(+), 2 deletions(-) create mode 100644 src/importexport/projectfilexmlparser.cpp create mode 100644 src/importexport/projectfilexmlparser.h create mode 100644 tests/importexport/projectfilexmltestdata.cpp create mode 100644 tests/importexport/projectfilexmltestdata.h create mode 100644 tests/importexport/tst_projectfilexmlparser.cpp create mode 100644 tests/importexport/tst_projectfilexmlparser.h diff --git a/src/importexport/projectfiledefinitions.h b/src/importexport/projectfiledefinitions.h index b7a5ffdf..84e76f7b 100644 --- a/src/importexport/projectfiledefinitions.h +++ b/src/importexport/projectfiledefinitions.h @@ -10,10 +10,26 @@ const char cModbusScopeTag[] = "modbusscope"; const char cModbusTag[] = "modbus"; const char cScopeTag[] = "scope"; const char cViewTag[] = "view"; +const char cConnectionTag[] = "connection"; const char cDeviceTag[] = "device"; const char cDeviceIdTag[] = "deviceid"; const char cDeviceNameTag[] = "name"; const char cLogTag[] = "log"; +const char cConnectionIdTag[] = "connectionid"; +const char cConnectionEnabledTag[] = "enabled"; +const char cConnectionTypeTag[] = "type"; +const char cIpTag[] = "ip"; +const char cPortTag[] = "port"; +const char cPortNameTag[] = "portname"; +const char cBaudrateTag[] = "baudrate"; +const char cParityTag[] = "parity"; +const char cDataBitsTag[] = "databits"; +const char cStopBitsTag[] = "stopbits"; +const char cSlaveIdTag[] = "slaveid"; +const char cTimeoutTag[] = "timeout"; +const char cConsecutiveMaxTag[] = "consecutivemax"; +const char cInt32LittleEndianTag[] = "int32littleendian"; +const char cPersistentConnectionTag[] = "persistentconnection"; const char cPollTimeTag[] = "polltime"; const char cAbsoluteTimesTag[] = "absolutetimes"; const char cLogToFileTag[] = "logtofile"; @@ -58,7 +74,9 @@ const char cAdapterSettingsKey[] = "settings"; const char cAdapterIdKey[] = "adapterId"; const char cAdapterKey[] = "adapter"; const char cIdJsonKey[] = "id"; +const char cConnectionsJsonKey[] = "connections"; const char cDevicesJsonKey[] = "devices"; +const char cConnectionTypeJsonKey[] = "connectiontype"; /* JSON constant values */ const quint32 cMinimumJsonVersion = 6; diff --git a/src/importexport/projectfilehandler.cpp b/src/importexport/projectfilehandler.cpp index d58757e8..03b764f6 100644 --- a/src/importexport/projectfilehandler.cpp +++ b/src/importexport/projectfilehandler.cpp @@ -4,6 +4,7 @@ #include "importexport/projectfiledata.h" #include "importexport/projectfilejsonexporter.h" #include "importexport/projectfilejsonparser.h" +#include "importexport/projectfilexmlparser.h" #include "models/device.h" #include "models/graphdatamodel.h" #include "models/guimodel.h" @@ -35,8 +36,22 @@ void ProjectFileHandler::openProjectFile(QString projectFilePath) QTextStream in(&file); QString projectFileContents = in.readAll(); - ProjectFileJsonParser jsonParser; - GeneralError parseErr = jsonParser.parseFile(projectFileContents, &loadedSettings); + GeneralError parseErr; + QString trimmed = projectFileContents.trimmed(); + if (trimmed.startsWith('{')) + { + ProjectFileJsonParser jsonParser; + parseErr = jsonParser.parseFile(projectFileContents, &loadedSettings); + } + else if (trimmed.startsWith('<')) + { + ProjectFileXmlParser xmlParser; + parseErr = xmlParser.parseFile(projectFileContents, &loadedSettings); + } + else + { + parseErr.reportError(tr("The file is not a valid MBS project file.")); + } if (parseErr.result()) { diff --git a/src/importexport/projectfilexmlparser.cpp b/src/importexport/projectfilexmlparser.cpp new file mode 100644 index 00000000..54fb8f4e --- /dev/null +++ b/src/importexport/projectfilexmlparser.cpp @@ -0,0 +1,800 @@ + +#include "projectfilexmlparser.h" + +#include "importexport/projectfiledefinitions.h" + +#include +#include +#include + +using ProjectFileData::AdapterFileSettings; +using ProjectFileData::DeviceSettings; +using ProjectFileData::GeneralSettings; +using ProjectFileData::LogSettings; +using ProjectFileData::ProjectSettings; +using ProjectFileData::RegisterSettings; +using ProjectFileData::ScaleSettings; +using ProjectFileData::ScopeSettings; +using ProjectFileData::ViewSettings; +using ProjectFileData::YAxisSettings; + +ProjectFileXmlParser::ProjectFileXmlParser() : _dataLevel(0) +{ +} + +/*! + * \brief Parse a legacy XML MBS project file (data levels 3–5). + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \return GeneralError — result() is true on success. + */ +GeneralError ProjectFileXmlParser::parseFile(const QString& fileContent, ProjectSettings* pSettings) +{ + GeneralError parseErr; + + QDomDocument::ParseResult result = + _domDocument.setContent(fileContent, QDomDocument::ParseOption::UseNamespaceProcessing); + if (!result) + { + parseErr.reportError(QString("Parse error at line %1, column %2:\n%3") + .arg(result.errorLine) + .arg(result.errorColumn) + .arg(result.errorMessage)); + return parseErr; + } + + QDomElement root = _domDocument.documentElement(); + if (root.tagName() != ProjectFileDefinitions::cModbusScopeTag) + { + parseErr.reportError(QString("The file is not a valid ModbusScope project file.")); + return parseErr; + } + + bool bRet; + QString strDataLevel = root.attribute(ProjectFileDefinitions::cDatalevelAttribute, "1"); + quint32 datalevel = strDataLevel.toUInt(&bRet); + + if (!bRet) + { + parseErr.reportError(QString("Data level (%1) is not a valid number").arg(strDataLevel)); + return parseErr; + } + + _dataLevel = datalevel; + if (datalevel < ProjectFileDefinitions::cMinimumDataLevel) + { + parseErr.reportError( + QString("Data level %1 is not supported. Minimum datalevel is %2.\n\n" + "Try saving the project file with a previous version of ModbusScope.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cMinimumDataLevel)); + return parseErr; + } + + if (datalevel > ProjectFileDefinitions::cCurrentDataLevel) + { + parseErr.reportError( + QString("Data level %1 is not supported. Only datalevel %2 or lower is supported.\n\n" + "Try upgrading ModbusScope to the latest version.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cCurrentDataLevel)); + return parseErr; + } + + QDomElement tag = root.firstChildElement(); + while (!tag.isNull()) + { + if (tag.tagName() == ProjectFileDefinitions::cModbusTag) + { + parseErr = parseModbusTag(tag, &pSettings->general); + if (!parseErr.result()) + { + break; + } + } + else if (tag.tagName() == ProjectFileDefinitions::cScopeTag) + { + parseErr = parseScopeTag(tag, &pSettings->scope); + if (!parseErr.result()) + { + break; + } + } + else if (tag.tagName() == ProjectFileDefinitions::cViewTag) + { + parseErr = parseViewTag(tag, &pSettings->view); + if (!parseErr.result()) + { + break; + } + } + + tag = tag.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ section, building both main-app settings and the adapter JSON blob. + */ +GeneralError ProjectFileXmlParser::parseModbusTag(const QDomElement& element, GeneralSettings* pGeneralSettings) +{ + GeneralError parseErr; + QJsonArray connectionsArray; + QJsonArray adapterDevicesArray; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cConnectionTag) + { + QJsonObject connectionJson; + + if (_dataLevel < 5) + { + QJsonObject deviceJson; + DeviceSettings deviceSettings; + + parseErr = parseLegacyConnectionTag(child, &connectionJson, &deviceJson, &deviceSettings); + + if (parseErr.result() && connectionJson[ProjectFileDefinitions::cEnabledAttribute].toBool(true)) + { + pGeneralSettings->deviceSettings.append(deviceSettings); + adapterDevicesArray.append(deviceJson); + } + } + else + { + parseErr = parseConnectionTag(child, &connectionJson); + } + + if (!parseErr.result()) + { + break; + } + + connectionsArray.append(connectionJson); + } + else if (child.tagName() == ProjectFileDefinitions::cDeviceTag) + { + DeviceSettings deviceSettings; + QJsonObject deviceJson; + + parseErr = parseDeviceTag(child, &deviceSettings, &deviceJson); + if (!parseErr.result()) + { + break; + } + + pGeneralSettings->deviceSettings.append(deviceSettings); + adapterDevicesArray.append(deviceJson); + } + else if (child.tagName() == ProjectFileDefinitions::cLogTag) + { + parseErr = parseLogTag(child, &pGeneralSettings->logSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + buildAdapterSettings(connectionsArray, adapterDevicesArray, pGeneralSettings); + } + + return parseErr; +} + +/*! + * \brief Parse a \c \ tag (data level 5) into a JSON object for the adapter blob. + */ +GeneralError ProjectFileXmlParser::parseConnectionTag(const QDomElement& element, QJsonObject* pConnectionJson) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull() && parseErr.result()) + { + parseConnectionFields(child, pConnectionJson); + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse a legacy \c \ tag (data level < 5) which contains both + * connection settings and device settings (slaveid, consecutivemax, etc.). + */ +GeneralError ProjectFileXmlParser::parseLegacyConnectionTag(const QDomElement& element, + QJsonObject* pConnectionJson, + QJsonObject* pDeviceJson, + DeviceSettings* pDeviceSettings) +{ + GeneralError parseErr; + + /* In legacy format, each connection implicitly defines a device. + * The device gets a synthetic deviceId based on the connection id. */ + quint32 connectionId = 0; + quint8 slaveId = 1; + quint8 consecutiveMax = 125; + bool int32LittleEndian = true; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + + parseConnectionFields(child, pConnectionJson); + + if (child.tagName() == ProjectFileDefinitions::cSlaveIdTag) + { + slaveId = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError(QString("Slave id ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cConsecutiveMaxTag) + { + consecutiveMax = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError( + QString("Consecutive register maximum ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cInt32LittleEndianTag) + { + int32LittleEndian = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + connectionId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection Id (%1) is not a valid number").arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + /* Build generic device settings for main app */ + pDeviceSettings->bDeviceId = true; + pDeviceSettings->deviceId = connectionId + 1; + pDeviceSettings->adapterType = "modbus"; + + /* Build adapter device JSON blob */ + (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(connectionId + 1); + (*pDeviceJson)[ProjectFileDefinitions::cConnectionIdTag] = static_cast(connectionId); + (*pDeviceJson)[ProjectFileDefinitions::cSlaveIdTag] = static_cast(slaveId); + (*pDeviceJson)[ProjectFileDefinitions::cConsecutiveMaxTag] = static_cast(consecutiveMax); + (*pDeviceJson)[ProjectFileDefinitions::cInt32LittleEndianTag] = int32LittleEndian; + } + + return parseErr; +} + +/*! + * \brief Parse a \c \ tag (data level 5) into both generic DeviceSettings and adapter JSON. + */ +GeneralError ProjectFileXmlParser::parseDeviceTag(const QDomElement& element, + DeviceSettings* pDeviceSettings, + QJsonObject* pDeviceJson) +{ + GeneralError parseErr; + + quint32 connectionId = 0; + quint8 slaveId = 1; + quint8 consecutiveMax = 125; + bool int32LittleEndian = true; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + if (child.tagName() == ProjectFileDefinitions::cDeviceIdTag) + { + pDeviceSettings->bDeviceId = true; + pDeviceSettings->deviceId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Device Id (%1) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cDeviceNameTag) + { + pDeviceSettings->bName = true; + pDeviceSettings->name = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + connectionId = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection Id (%1) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cSlaveIdTag) + { + slaveId = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError(QString("Slave id ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cConsecutiveMaxTag) + { + consecutiveMax = static_cast(child.text().toUInt(&bRet)); + if (!bRet) + { + parseErr.reportError( + QString("Consecutive register maximum ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cInt32LittleEndianTag) + { + int32LittleEndian = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + pDeviceSettings->adapterType = "modbus"; + + (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(pDeviceSettings->deviceId); + (*pDeviceJson)[ProjectFileDefinitions::cConnectionIdTag] = static_cast(connectionId); + (*pDeviceJson)[ProjectFileDefinitions::cSlaveIdTag] = static_cast(slaveId); + (*pDeviceJson)[ProjectFileDefinitions::cConsecutiveMaxTag] = static_cast(consecutiveMax); + (*pDeviceJson)[ProjectFileDefinitions::cInt32LittleEndianTag] = int32LittleEndian; + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag. + */ +GeneralError ProjectFileXmlParser::parseLogTag(const QDomElement& element, LogSettings* pLogSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cPollTimeTag) + { + bool bRet; + pLogSettings->bPollTime = true; + pLogSettings->pollTime = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Poll time ( %1 ) is not a valid number").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cAbsoluteTimesTag) + { + pLogSettings->bAbsoluteTimes = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cLogToFileTag) + { + parseErr = parseLogToFile(child, pLogSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ element. + */ +GeneralError ProjectFileXmlParser::parseLogToFile(const QDomElement& element, LogSettings* pLogSettings) +{ + GeneralError parseErr; + + QString enabled = element.attribute(ProjectFileDefinitions::cEnabledAttribute, ProjectFileDefinitions::cTrueValue); + pLogSettings->bLogToFile = (enabled.compare(ProjectFileDefinitions::cTrueValue, Qt::CaseInsensitive) == 0); + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cFilenameTag) + { + QFileInfo fileInfo = QFileInfo(child.text()); + bool bValid = true; + + if (!fileInfo.isFile()) + { + if (fileInfo.exists() || !fileInfo.dir().exists()) + { + bValid = false; + } + } + + if (bValid) + { + pLogSettings->bLogToFileFile = true; + pLogSettings->logFile = fileInfo.filePath(); + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag containing register definitions. + */ +GeneralError ProjectFileXmlParser::parseScopeTag(const QDomElement& element, ScopeSettings* pScopeSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cRegisterTag) + { + RegisterSettings registerData; + parseErr = parseRegisterTag(child, ®isterData); + if (!parseErr.result()) + { + break; + } + + pScopeSettings->registerList.append(registerData); + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse a single \c \ element. + */ +GeneralError ProjectFileXmlParser::parseRegisterTag(const QDomElement& element, + RegisterSettings* pRegisterSettings) +{ + GeneralError parseErr; + + QString active = element.attribute(ProjectFileDefinitions::cActiveAttribute, ProjectFileDefinitions::cTrueValue); + pRegisterSettings->bActive = (active.compare(ProjectFileDefinitions::cTrueValue, Qt::CaseInsensitive) == 0); + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cTextTag) + { + pRegisterSettings->text = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cColorTag) + { + if (QColor::isValidColorName(child.text())) + { + pRegisterSettings->bColor = true; + pRegisterSettings->color = QColor(child.text()); + } + else + { + parseErr.reportError(QString("Color is not a valid color. Did you use correct color format? " + "Expecting #FF0000 (red)")); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cValueAxisTag) + { + bool bOk = false; + quint32 axis = child.text().toUInt(&bOk); + if (bOk) + { + pRegisterSettings->valueAxis = axis; + } + } + else if (child.tagName() == ProjectFileDefinitions::cExpressionTag) + { + pRegisterSettings->expression = child.text(); + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag. + */ +GeneralError ProjectFileXmlParser::parseViewTag(const QDomElement& element, ViewSettings* pViewSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cScaleTag) + { + parseErr = parseScaleTag(child, &pViewSettings->scaleSettings); + if (!parseErr.result()) + { + break; + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the \c \ tag containing x-axis and y-axis settings. + */ +GeneralError ProjectFileXmlParser::parseScaleTag(const QDomElement& element, ScaleSettings* pScaleSettings) +{ + GeneralError parseErr; + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cXaxisTag) + { + QString mode = child.attribute(ProjectFileDefinitions::cModeAttribute); + + if (mode.compare(ProjectFileDefinitions::cSlidingValue, Qt::CaseInsensitive) == 0) + { + pScaleSettings->xAxis.bSliding = true; + parseErr = parseScaleXAxis(child, pScaleSettings); + if (!parseErr.result()) + { + break; + } + } + else + { + pScaleSettings->xAxis.bSliding = false; + } + } + else if (child.tagName() == ProjectFileDefinitions::cYaxisTag) + { + QString axisString = child.attribute(ProjectFileDefinitions::cAxisAttribute); + bool bRet = false; + int axisId = axisString.toInt(&bRet); + if (!bRet) + { + axisId = 0; + } + YAxisSettings* yAxisSettings = (axisId == 1) ? &pScaleSettings->y2Axis : &pScaleSettings->yAxis; + + QString mode = child.attribute(ProjectFileDefinitions::cModeAttribute); + + if (mode.compare(ProjectFileDefinitions::cWindowAutoValue, Qt::CaseInsensitive) == 0) + { + yAxisSettings->bWindowScale = true; + } + else if (mode.compare(ProjectFileDefinitions::cMinmaxValue, Qt::CaseInsensitive) == 0) + { + yAxisSettings->bMinMax = true; + parseErr = parseScaleYAxis(child, yAxisSettings); + if (!parseErr.result()) + { + break; + } + } + } + + child = child.nextSiblingElement(); + } + + return parseErr; +} + +/*! + * \brief Parse the x-axis sliding interval from a \c \ element. + */ +GeneralError ProjectFileXmlParser::parseScaleXAxis(const QDomElement& element, ScaleSettings* pScaleSettings) +{ + GeneralError parseErr; + bool bSlidingInterval = false; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + if (child.tagName() == ProjectFileDefinitions::cSlidingintervalTag) + { + bool bRet; + pScaleSettings->xAxis.slidingInterval = child.text().toUInt(&bRet); + if (bRet) + { + bSlidingInterval = true; + } + else + { + parseErr.reportError( + QString("Scale (x-axis) has an incorrect sliding interval. " + "\"%1\" is not a valid number") + .arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result() && !bSlidingInterval) + { + parseErr.reportError( + QString("If x-axis has sliding window scaling then slidinginterval variable should be defined.")); + } + + return parseErr; +} + +/*! + * \brief Parse min/max values from a \c \ element. + */ +GeneralError ProjectFileXmlParser::parseScaleYAxis(const QDomElement& element, YAxisSettings* pYAxisSettings) +{ + GeneralError parseErr; + bool bMin = false; + bool bMax = false; + + QDomElement child = element.firstChildElement(); + while (!child.isNull()) + { + bool bRet; + if (child.tagName() == ProjectFileDefinitions::cMinTag) + { + pYAxisSettings->scaleMin = QLocale().toDouble(child.text(), &bRet); + if (bRet) + { + bMin = true; + } + else + { + parseErr.reportError( + QString("Scale (y-axis) has an incorrect minimum. \"%1\" is not a valid double").arg(child.text())); + break; + } + } + else if (child.tagName() == ProjectFileDefinitions::cMaxTag) + { + pYAxisSettings->scaleMax = QLocale().toDouble(child.text(), &bRet); + if (bRet) + { + bMax = true; + } + else + { + parseErr.reportError( + QString("Scale (y-axis) has an incorrect maximum. \"%1\" is not a valid double").arg(child.text())); + break; + } + } + + child = child.nextSiblingElement(); + } + + if (parseErr.result()) + { + if (!bMin) + { + parseErr.reportError( + QString("If y-axis has min max scaling then min variable should be defined.")); + } + else if (!bMax) + { + parseErr.reportError( + QString("If y-axis has min max scaling then max variable should be defined.")); + } + } + + return parseErr; +} + +/*! + * \brief Extract connection fields from a child element into a JSON object for the adapter blob. + */ +void ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson) +{ + bool bRet; + + if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionEnabledTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cEnabledAttribute] = + (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } + else if (child.tagName() == ProjectFileDefinitions::cConnectionTypeTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cConnectionTypeJsonKey] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cIpTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cIpTag] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cPortTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cPortTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cPortNameTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cPortNameTag] = child.text(); + } + else if (child.tagName() == ProjectFileDefinitions::cBaudrateTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cBaudrateTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cParityTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cParityTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cStopBitsTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cStopBitsTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cDataBitsTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cDataBitsTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cTimeoutTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cTimeoutTag] = static_cast(child.text().toUInt(&bRet)); + } + else if (child.tagName() == ProjectFileDefinitions::cPersistentConnectionTag) + { + (*pConnectionJson)[ProjectFileDefinitions::cPersistentConnectionTag] = + (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); + } +} + +/*! + * \brief Construct the adapter settings entry from collected connection and device JSON arrays. + */ +void ProjectFileXmlParser::buildAdapterSettings(const QJsonArray& connectionsArray, + const QJsonArray& devicesArray, + GeneralSettings* pGeneralSettings) +{ + AdapterFileSettings adapterSettings; + adapterSettings.type = "modbus"; + + QJsonObject settingsObj; + settingsObj[ProjectFileDefinitions::cConnectionsJsonKey] = connectionsArray; + settingsObj[ProjectFileDefinitions::cDevicesJsonKey] = devicesArray; + adapterSettings.settings = settingsObj; + + pGeneralSettings->adapterList.append(adapterSettings); + + /* Set adapterId on all devices to point to adapter index 0 */ + for (int i = 0; i < pGeneralSettings->deviceSettings.size(); i++) + { + pGeneralSettings->deviceSettings[i].adapterId = 0; + pGeneralSettings->deviceSettings[i].adapterType = "modbus"; + } +} diff --git a/src/importexport/projectfilexmlparser.h b/src/importexport/projectfilexmlparser.h new file mode 100644 index 00000000..d0fa989e --- /dev/null +++ b/src/importexport/projectfilexmlparser.h @@ -0,0 +1,65 @@ +#ifndef PROJECTFILEXMLPARSER_H +#define PROJECTFILEXMLPARSER_H + +#include "importexport/generalerror.h" +#include "importexport/projectfiledata.h" + +#include +#include +#include +#include + +/*! + * \brief Parses a legacy XML-format MBS project file (data levels 3–5) and + * converts it into the same ProjectSettings structure used by the JSON parser. + * + * Connection and device settings from the XML \c \ section are converted + * into an adapter settings JSON blob so the result is indistinguishable from a + * file loaded via ProjectFileJsonParser. + */ +class ProjectFileXmlParser +{ +public: + ProjectFileXmlParser(); + + /*! + * \brief Parse a legacy XML MBS project file into ProjectSettings. + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \return GeneralError — result() is true on success. + */ + GeneralError parseFile(const QString& fileContent, ProjectFileData::ProjectSettings* pSettings); + +private: + GeneralError parseModbusTag(const QDomElement& element, ProjectFileData::GeneralSettings* pGeneralSettings); + + GeneralError parseConnectionTag(const QDomElement& element, QJsonObject* pConnectionJson); + GeneralError parseLegacyConnectionTag(const QDomElement& element, + QJsonObject* pConnectionJson, + QJsonObject* pDeviceJson, + ProjectFileData::DeviceSettings* pDeviceSettings); + GeneralError parseDeviceTag(const QDomElement& element, + ProjectFileData::DeviceSettings* pDeviceSettings, + QJsonObject* pDeviceJson); + GeneralError parseLogTag(const QDomElement& element, ProjectFileData::LogSettings* pLogSettings); + GeneralError parseLogToFile(const QDomElement& element, ProjectFileData::LogSettings* pLogSettings); + + GeneralError parseScopeTag(const QDomElement& element, ProjectFileData::ScopeSettings* pScopeSettings); + GeneralError parseRegisterTag(const QDomElement& element, ProjectFileData::RegisterSettings* pRegisterSettings); + + GeneralError parseViewTag(const QDomElement& element, ProjectFileData::ViewSettings* pViewSettings); + GeneralError parseScaleTag(const QDomElement& element, ProjectFileData::ScaleSettings* pScaleSettings); + GeneralError parseScaleXAxis(const QDomElement& element, ProjectFileData::ScaleSettings* pScaleSettings); + GeneralError parseScaleYAxis(const QDomElement& element, ProjectFileData::YAxisSettings* pYAxisSettings); + + void parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson); + + void buildAdapterSettings(const QJsonArray& connectionsArray, + const QJsonArray& devicesArray, + ProjectFileData::GeneralSettings* pGeneralSettings); + + QDomDocument _domDocument; + quint32 _dataLevel; +}; + +#endif // PROJECTFILEXMLPARSER_H diff --git a/tests/importexport/CMakeLists.txt b/tests/importexport/CMakeLists.txt index cf9c1b33..f0196459 100644 --- a/tests/importexport/CMakeLists.txt +++ b/tests/importexport/CMakeLists.txt @@ -5,6 +5,7 @@ add_xtest(tst_datafileparser ${CMAKE_CURRENT_SOURCE_DIR}/csvdata.cpp) add_xtest_mock(tst_presethandler) add_xtest(tst_presetparser ${CMAKE_CURRENT_SOURCE_DIR}/presetfiletestdata.cpp) add_xtest(tst_projectfilejsonparser ${CMAKE_CURRENT_SOURCE_DIR}/projectfilejsontestdata.cpp) +add_xtest(tst_projectfilexmlparser ${CMAKE_CURRENT_SOURCE_DIR}/projectfilexmltestdata.cpp) add_xtest(tst_projectfilejsonexporter) add_xtest(tst_projectfilehandler) add_xtest(tst_settingsauto ${CMAKE_CURRENT_SOURCE_DIR}/csvdata.cpp) diff --git a/tests/importexport/projectfilexmltestdata.cpp b/tests/importexport/projectfilexmltestdata.cpp new file mode 100644 index 00000000..f37d11ee --- /dev/null +++ b/tests/importexport/projectfilexmltestdata.cpp @@ -0,0 +1,277 @@ + +#include "projectfilexmltestdata.h" + +// clang-format off + +QString ProjectFileXmlTestData::cTooLowDataLevel = QString( + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cTooHighDataLevel = QString( + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel3Expressions = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " #ff0000 \n" + " \n" + " \n" + " Data point 2 \n" + " \n" + " #0000ff \n" + " \n" + " \n" + " Data point 3 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel5Connection = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 1000 \n" + " true \n" + " \n" + " \n" + " 1 \n" + " Device 1 \n" + " 0 \n" + " 3 \n" + " 100 \n" + " false \n" + " \n" + " \n" + " 250 \n" + " false \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel5MixedMulti = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " serial \n" + " 127.0.0.1 \n" + " 502 \n" + " COM10 \n" + " 38400 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 500 \n" + " true \n" + " \n" + " \n" + " true \n" + " 1 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 2000 \n" + " true \n" + " \n" + " \n" + " false \n" + " 2 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 1000 \n" + " true \n" + " \n" + " \n" + " 1 \n" + " Device 1 (serial 1) \n" + " 0 \n" + " 1 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 2 \n" + " Device 2 (serial 2) \n" + " 0 \n" + " 2 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 3 \n" + " Device 3 (TCP) \n" + " 1 \n" + " 1 \n" + " 125 \n" + " true \n" + " \n" + " \n" + " 250 \n" + " false \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cDataLevel4LegacyConnection = QString( + " \n" + " \n" + " \n" + " \n" + " true \n" + " 0 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 2 \n" + " 1002 \n" + " 122 \n" + " true \n" + " true \n" + " \n" + " \n" + " false \n" + " 1 \n" + " tcp \n" + " 127.0.0.1 \n" + " 502 \n" + " COM1 \n" + " 115200 \n" + " 0 \n" + " 1 \n" + " 8 \n" + " 3 \n" + " 1000 \n" + " 125 \n" + " true \n" + " true \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cLogSettings = QString( + " \n" + " \n" + " \n" + " \n" + " 750 \n" + " true \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleSliding = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " 20 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleMinMax = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " 20 \n" + " \n" + " \n" + " 0 \n" + " 25,5 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cScaleWindowAuto = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " 0 \n" + " 25,5 \n" + " \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cValueAxis = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " 0 \n" + " \n" + " \n" + " Data point 2 \n" + " \n" + " 1 \n" + " \n" + " \n" + " Data point 3 \n" + " \n" + " \n" + " \n" + " \n" +); + +// clang-format on diff --git a/tests/importexport/projectfilexmltestdata.h b/tests/importexport/projectfilexmltestdata.h new file mode 100644 index 00000000..8d2c3910 --- /dev/null +++ b/tests/importexport/projectfilexmltestdata.h @@ -0,0 +1,24 @@ +#ifndef PROJECTFILEXMLTESTDATA_H +#define PROJECTFILEXMLTESTDATA_H + +#include + +class ProjectFileXmlTestData +{ +public: + static QString cTooLowDataLevel; + static QString cTooHighDataLevel; + + static QString cDataLevel3Expressions; + static QString cDataLevel5Connection; + static QString cDataLevel5MixedMulti; + static QString cDataLevel4LegacyConnection; + + static QString cLogSettings; + static QString cScaleSliding; + static QString cScaleMinMax; + static QString cScaleWindowAuto; + static QString cValueAxis; +}; + +#endif // PROJECTFILEXMLTESTDATA_H diff --git a/tests/importexport/tst_projectfilexmlparser.cpp b/tests/importexport/tst_projectfilexmlparser.cpp new file mode 100644 index 00000000..cf0bbd2f --- /dev/null +++ b/tests/importexport/tst_projectfilexmlparser.cpp @@ -0,0 +1,277 @@ + +#include "tst_projectfilexmlparser.h" + +#include "importexport/projectfilexmlparser.h" +#include "projectfilexmltestdata.h" + +#include +#include +#include +#include + +using ProjectFileData::ProjectSettings; + +void TestProjectFileXmlParser::init() +{ +} + +void TestProjectFileXmlParser::initTestCase() +{ + QLocale::setDefault(QLocale(QLocale::Dutch, QLocale::Belgium)); +} + +void TestProjectFileXmlParser::cleanup() +{ +} + +void TestProjectFileXmlParser::tooLowDataLevel() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cTooLowDataLevel, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::tooHighDataLevel() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cTooHighDataLevel, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::dataLevel3Expressions() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel3Expressions, &settings); + QVERIFY(err.result()); + + QCOMPARE(settings.scope.registerList.size(), 3); + + QVERIFY(settings.scope.registerList[0].bActive); + QCOMPARE(settings.scope.registerList[0].text, QString("Data point")); + QCOMPARE(settings.scope.registerList[0].expression, QString("${40001}/2")); + QVERIFY(settings.scope.registerList[0].bColor); + QCOMPARE(settings.scope.registerList[0].color, QColor("#ff0000")); + + QVERIFY(settings.scope.registerList[1].bActive); + QCOMPARE(settings.scope.registerList[1].text, QString("Data point 2")); + QCOMPARE(settings.scope.registerList[1].expression, QString("${40002:s16b}")); + QVERIFY(settings.scope.registerList[1].bColor); + QCOMPARE(settings.scope.registerList[1].color, QColor("#0000ff")); + + QVERIFY(!settings.scope.registerList[2].bActive); + QCOMPARE(settings.scope.registerList[2].text, QString("Data point 3")); + QCOMPARE(settings.scope.registerList[2].expression, QString("${40003@2:s16b}*10")); +} + +void TestProjectFileXmlParser::dataLevel5Connection() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel5Connection, &settings); + QVERIFY(err.result()); + + /* Adapter blob */ + QCOMPARE(settings.general.adapterList.size(), 1); + QCOMPARE(settings.general.adapterList[0].type, QString("modbus")); + + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QVERIFY(adapterSettings.contains("connections")); + QVERIFY(adapterSettings.contains("devices")); + + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 1); + + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["id"].toInt(), 0); + QCOMPARE(conn0["enabled"].toBool(), true); + QCOMPARE(conn0["connectiontype"].toString(), QString("tcp")); + QCOMPARE(conn0["ip"].toString(), QString("127.0.0.1")); + QCOMPARE(conn0["port"].toInt(), 502); + QCOMPARE(conn0["portname"].toString(), QString("COM1")); + QCOMPARE(conn0["baudrate"].toInt(), 115200); + QCOMPARE(conn0["parity"].toInt(), 0); + QCOMPARE(conn0["stopbits"].toInt(), 1); + QCOMPARE(conn0["databits"].toInt(), 8); + QCOMPARE(conn0["timeout"].toInt(), 1000); + QVERIFY(conn0["persistentconnection"].toBool()); + + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 1); + QJsonObject dev0 = adapterDevices[0].toObject(); + QCOMPARE(dev0["id"].toInt(), 1); + QCOMPARE(dev0["connectionid"].toInt(), 0); + QCOMPARE(dev0["slaveid"].toInt(), 3); + QCOMPARE(dev0["consecutivemax"].toInt(), 100); + QVERIFY(!dev0["int32littleendian"].toBool()); + + /* Generic devices */ + QCOMPARE(settings.general.deviceSettings.size(), 1); + QVERIFY(settings.general.deviceSettings[0].bDeviceId); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + QCOMPARE(settings.general.deviceSettings[0].adapterId, static_cast(0)); + QCOMPARE(settings.general.deviceSettings[0].adapterType, QString("modbus")); + QVERIFY(settings.general.deviceSettings[0].bName); + QCOMPARE(settings.general.deviceSettings[0].name, QString("Device 1")); + + /* Log */ + QVERIFY(settings.general.logSettings.bPollTime); + QCOMPARE(settings.general.logSettings.pollTime, static_cast(250)); + QVERIFY(!settings.general.logSettings.bAbsoluteTimes); + QVERIFY(settings.general.logSettings.bLogToFile); +} + +void TestProjectFileXmlParser::dataLevel5MixedMulti() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel5MixedMulti, &settings); + QVERIFY(err.result()); + + /* 3 connections, 3 devices */ + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 3); + + /* First connection: serial */ + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["connectiontype"].toString(), QString("serial")); + QCOMPARE(conn0["portname"].toString(), QString("COM10")); + QCOMPARE(conn0["baudrate"].toInt(), 38400); + QCOMPARE(conn0["timeout"].toInt(), 500); + + /* Third connection: disabled */ + QJsonObject conn2 = connections[2].toObject(); + QVERIFY(!conn2["enabled"].toBool()); + + /* 3 generic devices */ + QCOMPARE(settings.general.deviceSettings.size(), 3); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + QCOMPARE(settings.general.deviceSettings[0].name, QString("Device 1 (serial 1)")); + QCOMPARE(settings.general.deviceSettings[1].deviceId, static_cast(2)); + QCOMPARE(settings.general.deviceSettings[1].name, QString("Device 2 (serial 2)")); + QCOMPARE(settings.general.deviceSettings[2].deviceId, static_cast(3)); + QCOMPARE(settings.general.deviceSettings[2].name, QString("Device 3 (TCP)")); + + /* Adapter devices */ + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 3); + QCOMPARE(adapterDevices[0].toObject()["connectionid"].toInt(), 0); + QCOMPARE(adapterDevices[0].toObject()["slaveid"].toInt(), 1); + QCOMPARE(adapterDevices[2].toObject()["connectionid"].toInt(), 1); +} + +void TestProjectFileXmlParser::dataLevel4LegacyConnection() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cDataLevel4LegacyConnection, &settings); + QVERIFY(err.result()); + + /* Only enabled connections create devices in legacy mode */ + QCOMPARE(settings.general.deviceSettings.size(), 1); + QVERIFY(settings.general.deviceSettings[0].bDeviceId); + QCOMPARE(settings.general.deviceSettings[0].deviceId, static_cast(1)); + + /* Adapter blob should have both connections */ + QJsonObject adapterSettings = settings.general.adapterList[0].settings; + QJsonArray connections = adapterSettings["connections"].toArray(); + QCOMPARE(connections.size(), 2); + + /* First connection TCP */ + QJsonObject conn0 = connections[0].toObject(); + QCOMPARE(conn0["connectiontype"].toString(), QString("tcp")); + QCOMPARE(conn0["timeout"].toInt(), 1002); + + /* Only one adapter device (from the enabled connection) */ + QJsonArray adapterDevices = adapterSettings["devices"].toArray(); + QCOMPARE(adapterDevices.size(), 1); + QCOMPARE(adapterDevices[0].toObject()["slaveid"].toInt(), 2); + QCOMPARE(adapterDevices[0].toObject()["consecutivemax"].toInt(), 122); + QVERIFY(adapterDevices[0].toObject()["int32littleendian"].toBool()); +} + +void TestProjectFileXmlParser::logSettings() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cLogSettings, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.general.logSettings.bPollTime); + QCOMPARE(settings.general.logSettings.pollTime, static_cast(750)); + QVERIFY(settings.general.logSettings.bAbsoluteTimes); + QVERIFY(!settings.general.logSettings.bLogToFile); +} + +void TestProjectFileXmlParser::scaleSliding() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleSliding, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.xAxis.bSliding); + QCOMPARE(settings.view.scaleSettings.xAxis.slidingInterval, static_cast(20)); +} + +void TestProjectFileXmlParser::scaleMinMax() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleMinMax, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.xAxis.bSliding); + QCOMPARE(settings.view.scaleSettings.xAxis.slidingInterval, static_cast(20)); + + QVERIFY(settings.view.scaleSettings.yAxis.bMinMax); + QCOMPARE(settings.view.scaleSettings.yAxis.scaleMin, 0.0); + QCOMPARE(settings.view.scaleSettings.yAxis.scaleMax, 25.5); +} + +void TestProjectFileXmlParser::scaleWindowAuto() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cScaleWindowAuto, &settings); + QVERIFY(err.result()); + + QVERIFY(settings.view.scaleSettings.yAxis.bWindowScale); + QVERIFY(!settings.view.scaleSettings.yAxis.bMinMax); + + QVERIFY(settings.view.scaleSettings.y2Axis.bMinMax); + QCOMPARE(settings.view.scaleSettings.y2Axis.scaleMin, 0.0); + QCOMPARE(settings.view.scaleSettings.y2Axis.scaleMax, 25.5); +} + +void TestProjectFileXmlParser::valueAxis() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cValueAxis, &settings); + QVERIFY(err.result()); + + QCOMPARE(settings.scope.registerList.size(), 3); + + QCOMPARE(settings.scope.registerList[0].valueAxis, static_cast(0)); + QCOMPARE(settings.scope.registerList[1].valueAxis, static_cast(1)); + QCOMPARE(settings.scope.registerList[2].valueAxis, static_cast(0)); +} + +QTEST_MAIN(TestProjectFileXmlParser) + +#include "tst_projectfilexmlparser.moc" diff --git a/tests/importexport/tst_projectfilexmlparser.h b/tests/importexport/tst_projectfilexmlparser.h new file mode 100644 index 00000000..6ba542cb --- /dev/null +++ b/tests/importexport/tst_projectfilexmlparser.h @@ -0,0 +1,33 @@ + +#ifndef TST_PROJECTFILEXMLPARSER_H +#define TST_PROJECTFILEXMLPARSER_H + +#include + +class TestProjectFileXmlParser : public QObject +{ + Q_OBJECT +private slots: + void init(); + void initTestCase(); + void cleanup(); + + void tooLowDataLevel(); + void tooHighDataLevel(); + + void dataLevel3Expressions(); + + void dataLevel5Connection(); + void dataLevel5MixedMulti(); + void dataLevel4LegacyConnection(); + + void logSettings(); + + void scaleSliding(); + void scaleMinMax(); + void scaleWindowAuto(); + + void valueAxis(); +}; + +#endif // TST_PROJECTFILEXMLPARSER_H From 320e2f322d727d85992e25902314f67b9fabae9b Mon Sep 17 00:00:00 2001 From: Jens Geudens Date: Fri, 10 Apr 2026 22:10:30 +0200 Subject: [PATCH 2/4] Formatting and quality --- src/communication/modbuspoll.cpp | 2 -- src/dialogs/addregisterwidget.cpp | 3 +- src/importexport/projectfilexmlparser.cpp | 38 ++++++++++------------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/communication/modbuspoll.cpp b/src/communication/modbuspoll.cpp index 9121d1f6..33ea1296 100644 --- a/src/communication/modbuspoll.cpp +++ b/src/communication/modbuspoll.cpp @@ -5,7 +5,6 @@ #include "util/formatdatetime.h" #include "util/scopelogging.h" - ModbusPoll::ModbusPoll(SettingsModel* pSettingsModel, QObject* parent) : QObject(parent), _bPollActive(false) { _pPollTimer = new QTimer(this); @@ -107,7 +106,6 @@ void ModbusPoll::onReadDataResult(ResultDoubleList results) } } - /*! \brief Returns the AdapterManager owned by this instance. */ AdapterManager* ModbusPoll::adapterManager() const { diff --git a/src/dialogs/addregisterwidget.cpp b/src/dialogs/addregisterwidget.cpp index c3b14e7c..efe8b5b0 100644 --- a/src/dialogs/addregisterwidget.cpp +++ b/src/dialogs/addregisterwidget.cpp @@ -77,7 +77,8 @@ AddRegisterWidget::AddRegisterWidget(SettingsModel* pSettingsModel, } connect(_pUi->btnAdd, &QPushButton::clicked, this, &AddRegisterWidget::handleResultAccept); - connect(_pAdapterManager, &AdapterManager::buildExpressionResult, this, &AddRegisterWidget::onBuildExpressionResult); + connect(_pAdapterManager, &AdapterManager::buildExpressionResult, this, + &AddRegisterWidget::onBuildExpressionResult); _axisGroup.setExclusive(true); _axisGroup.addButton(_pUi->radioPrimary); diff --git a/src/importexport/projectfilexmlparser.cpp b/src/importexport/projectfilexmlparser.cpp index 54fb8f4e..fa950d10 100644 --- a/src/importexport/projectfilexmlparser.cpp +++ b/src/importexport/projectfilexmlparser.cpp @@ -63,23 +63,21 @@ GeneralError ProjectFileXmlParser::parseFile(const QString& fileContent, Project _dataLevel = datalevel; if (datalevel < ProjectFileDefinitions::cMinimumDataLevel) { - parseErr.reportError( - QString("Data level %1 is not supported. Minimum datalevel is %2.\n\n" - "Try saving the project file with a previous version of ModbusScope.\n\n" - "Project file loading is aborted.") - .arg(datalevel) - .arg(ProjectFileDefinitions::cMinimumDataLevel)); + parseErr.reportError(QString("Data level %1 is not supported. Minimum datalevel is %2.\n\n" + "Try saving the project file with a previous version of ModbusScope.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cMinimumDataLevel)); return parseErr; } if (datalevel > ProjectFileDefinitions::cCurrentDataLevel) { - parseErr.reportError( - QString("Data level %1 is not supported. Only datalevel %2 or lower is supported.\n\n" - "Try upgrading ModbusScope to the latest version.\n\n" - "Project file loading is aborted.") - .arg(datalevel) - .arg(ProjectFileDefinitions::cCurrentDataLevel)); + parseErr.reportError(QString("Data level %1 is not supported. Only datalevel %2 or lower is supported.\n\n" + "Try upgrading ModbusScope to the latest version.\n\n" + "Project file loading is aborted.") + .arg(datalevel) + .arg(ProjectFileDefinitions::cCurrentDataLevel)); return parseErr; } @@ -478,8 +476,7 @@ GeneralError ProjectFileXmlParser::parseScopeTag(const QDomElement& element, Sco /*! * \brief Parse a single \c \ element. */ -GeneralError ProjectFileXmlParser::parseRegisterTag(const QDomElement& element, - RegisterSettings* pRegisterSettings) +GeneralError ProjectFileXmlParser::parseRegisterTag(const QDomElement& element, RegisterSettings* pRegisterSettings) { GeneralError parseErr; @@ -633,10 +630,9 @@ GeneralError ProjectFileXmlParser::parseScaleXAxis(const QDomElement& element, S } else { - parseErr.reportError( - QString("Scale (x-axis) has an incorrect sliding interval. " - "\"%1\" is not a valid number") - .arg(child.text())); + parseErr.reportError(QString("Scale (x-axis) has an incorrect sliding interval. " + "\"%1\" is not a valid number") + .arg(child.text())); break; } } @@ -702,13 +698,11 @@ GeneralError ProjectFileXmlParser::parseScaleYAxis(const QDomElement& element, Y { if (!bMin) { - parseErr.reportError( - QString("If y-axis has min max scaling then min variable should be defined.")); + parseErr.reportError(QString("If y-axis has min max scaling then min variable should be defined.")); } else if (!bMax) { - parseErr.reportError( - QString("If y-axis has min max scaling then max variable should be defined.")); + parseErr.reportError(QString("If y-axis has min max scaling then max variable should be defined.")); } } From c93116671212b55ac8173b6f1ebb32e598d3d8f2 Mon Sep 17 00:00:00 2001 From: Jens Geudens Date: Fri, 10 Apr 2026 22:28:13 +0200 Subject: [PATCH 3/4] Add error handling + fix check in tests --- src/importexport/projectfilexmlparser.cpp | 70 ++++++++++++++++--- src/importexport/projectfilexmlparser.h | 2 +- .../importexport/tst_projectfilexmlparser.cpp | 4 +- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/importexport/projectfilexmlparser.cpp b/src/importexport/projectfilexmlparser.cpp index fa950d10..12bd18ae 100644 --- a/src/importexport/projectfilexmlparser.cpp +++ b/src/importexport/projectfilexmlparser.cpp @@ -199,7 +199,7 @@ GeneralError ProjectFileXmlParser::parseConnectionTag(const QDomElement& element QDomElement child = element.firstChildElement(); while (!child.isNull() && parseErr.result()) { - parseConnectionFields(child, pConnectionJson); + parseErr = parseConnectionFields(child, pConnectionJson); child = child.nextSiblingElement(); } @@ -229,7 +229,11 @@ GeneralError ProjectFileXmlParser::parseLegacyConnectionTag(const QDomElement& e { bool bRet; - parseConnectionFields(child, pConnectionJson); + parseErr = parseConnectionFields(child, pConnectionJson); + if (!parseErr.result()) + { + break; + } if (child.tagName() == ProjectFileDefinitions::cSlaveIdTag) { @@ -711,14 +715,22 @@ GeneralError ProjectFileXmlParser::parseScaleYAxis(const QDomElement& element, Y /*! * \brief Extract connection fields from a child element into a JSON object for the adapter blob. + * \return GeneralError — result() is false if a numeric field contains an invalid value. */ -void ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson) +GeneralError ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson) { + GeneralError parseErr; bool bRet; if (child.tagName() == ProjectFileDefinitions::cConnectionIdTag) { - (*pConnectionJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Connection id (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cConnectionEnabledTag) { @@ -735,7 +747,13 @@ void ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJson } else if (child.tagName() == ProjectFileDefinitions::cPortTag) { - (*pConnectionJson)[ProjectFileDefinitions::cPortTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Port (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cPortTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cPortNameTag) { @@ -743,29 +761,61 @@ void ProjectFileXmlParser::parseConnectionFields(const QDomElement& child, QJson } else if (child.tagName() == ProjectFileDefinitions::cBaudrateTag) { - (*pConnectionJson)[ProjectFileDefinitions::cBaudrateTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Baudrate (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cBaudrateTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cParityTag) { - (*pConnectionJson)[ProjectFileDefinitions::cParityTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Parity (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cParityTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cStopBitsTag) { - (*pConnectionJson)[ProjectFileDefinitions::cStopBitsTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Stop bits (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cStopBitsTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cDataBitsTag) { - (*pConnectionJson)[ProjectFileDefinitions::cDataBitsTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Data bits (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cDataBitsTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cTimeoutTag) { - (*pConnectionJson)[ProjectFileDefinitions::cTimeoutTag] = static_cast(child.text().toUInt(&bRet)); + quint32 val = child.text().toUInt(&bRet); + if (!bRet) + { + parseErr.reportError(QString("Timeout (%1) is not a valid number").arg(child.text())); + return parseErr; + } + (*pConnectionJson)[ProjectFileDefinitions::cTimeoutTag] = static_cast(val); } else if (child.tagName() == ProjectFileDefinitions::cPersistentConnectionTag) { (*pConnectionJson)[ProjectFileDefinitions::cPersistentConnectionTag] = (child.text().toLower().compare(ProjectFileDefinitions::cTrueValue) == 0); } + + return parseErr; } /*! diff --git a/src/importexport/projectfilexmlparser.h b/src/importexport/projectfilexmlparser.h index d0fa989e..83451f41 100644 --- a/src/importexport/projectfilexmlparser.h +++ b/src/importexport/projectfilexmlparser.h @@ -52,7 +52,7 @@ class ProjectFileXmlParser GeneralError parseScaleXAxis(const QDomElement& element, ProjectFileData::ScaleSettings* pScaleSettings); GeneralError parseScaleYAxis(const QDomElement& element, ProjectFileData::YAxisSettings* pYAxisSettings); - void parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson); + GeneralError parseConnectionFields(const QDomElement& child, QJsonObject* pConnectionJson); void buildAdapterSettings(const QJsonArray& connectionsArray, const QJsonArray& devicesArray, diff --git a/tests/importexport/tst_projectfilexmlparser.cpp b/tests/importexport/tst_projectfilexmlparser.cpp index cf0bbd2f..5b0d78ee 100644 --- a/tests/importexport/tst_projectfilexmlparser.cpp +++ b/tests/importexport/tst_projectfilexmlparser.cpp @@ -237,7 +237,7 @@ void TestProjectFileXmlParser::scaleMinMax() QCOMPARE(settings.view.scaleSettings.xAxis.slidingInterval, static_cast(20)); QVERIFY(settings.view.scaleSettings.yAxis.bMinMax); - QCOMPARE(settings.view.scaleSettings.yAxis.scaleMin, 0.0); + QVERIFY(qFuzzyIsNull(settings.view.scaleSettings.yAxis.scaleMin)); QCOMPARE(settings.view.scaleSettings.yAxis.scaleMax, 25.5); } @@ -253,7 +253,7 @@ void TestProjectFileXmlParser::scaleWindowAuto() QVERIFY(!settings.view.scaleSettings.yAxis.bMinMax); QVERIFY(settings.view.scaleSettings.y2Axis.bMinMax); - QCOMPARE(settings.view.scaleSettings.y2Axis.scaleMin, 0.0); + QVERIFY(qFuzzyIsNull(settings.view.scaleSettings.y2Axis.scaleMin)); QCOMPARE(settings.view.scaleSettings.y2Axis.scaleMax, 25.5); } From 2501d7649b8e7ca249b0ff428415c05cc8a262eb Mon Sep 17 00:00:00 2001 From: Jens Geudens Date: Mon, 13 Apr 2026 19:43:41 +0200 Subject: [PATCH 4/4] Fix three correctness issues in ProjectFileXmlParser - Resolve relative log-file paths against the project base directory instead of the process CWD; pass the base dir from the file handler through parseFile and use it in parseLogToFile - Report a parse error (instead of silently ignoring) when contains a non-numeric value, consistent with all other numeric fields - Compute the real adapter index after appending rather than hardcoding 0; centralise adapterType/adapterId assignment in buildAdapterSettings and guard with adapterType.isEmpty() so pre-linked devices are skipped Adds tests: valueAxisInvalid, logFileRelativePath Co-Authored-By: Claude Sonnet 4.6 --- src/importexport/projectfilehandler.cpp | 3 +- src/importexport/projectfilexmlparser.cpp | 33 +++++++++++----- src/importexport/projectfilexmlparser.h | 12 ++++-- tests/importexport/projectfilexmltestdata.cpp | 26 +++++++++++++ tests/importexport/projectfilexmltestdata.h | 2 + .../importexport/tst_projectfilexmlparser.cpp | 38 +++++++++++++++++++ tests/importexport/tst_projectfilexmlparser.h | 3 ++ 7 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/importexport/projectfilehandler.cpp b/src/importexport/projectfilehandler.cpp index 03b764f6..3a6c9b63 100644 --- a/src/importexport/projectfilehandler.cpp +++ b/src/importexport/projectfilehandler.cpp @@ -46,7 +46,8 @@ void ProjectFileHandler::openProjectFile(QString projectFilePath) else if (trimmed.startsWith('<')) { ProjectFileXmlParser xmlParser; - parseErr = xmlParser.parseFile(projectFileContents, &loadedSettings); + parseErr = + xmlParser.parseFile(projectFileContents, &loadedSettings, QFileInfo(projectFilePath).absolutePath()); } else { diff --git a/src/importexport/projectfilexmlparser.cpp b/src/importexport/projectfilexmlparser.cpp index 12bd18ae..a8ceaa2a 100644 --- a/src/importexport/projectfilexmlparser.cpp +++ b/src/importexport/projectfilexmlparser.cpp @@ -24,13 +24,18 @@ ProjectFileXmlParser::ProjectFileXmlParser() : _dataLevel(0) /*! * \brief Parse a legacy XML MBS project file (data levels 3–5). - * \param fileContent Raw file contents. - * \param pSettings Output settings structure. + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \param projectBaseDir Absolute path of the directory containing the project file, + * used to resolve relative log-file paths. * \return GeneralError — result() is true on success. */ -GeneralError ProjectFileXmlParser::parseFile(const QString& fileContent, ProjectSettings* pSettings) +GeneralError ProjectFileXmlParser::parseFile(const QString& fileContent, + ProjectSettings* pSettings, + const QString& projectBaseDir) { GeneralError parseErr; + _projectBaseDir = projectBaseDir; QDomDocument::ParseResult result = _domDocument.setContent(fileContent, QDomDocument::ParseOption::UseNamespaceProcessing); @@ -276,7 +281,6 @@ GeneralError ProjectFileXmlParser::parseLegacyConnectionTag(const QDomElement& e /* Build generic device settings for main app */ pDeviceSettings->bDeviceId = true; pDeviceSettings->deviceId = connectionId + 1; - pDeviceSettings->adapterType = "modbus"; /* Build adapter device JSON blob */ (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(connectionId + 1); @@ -360,8 +364,6 @@ GeneralError ProjectFileXmlParser::parseDeviceTag(const QDomElement& element, if (parseErr.result()) { - pDeviceSettings->adapterType = "modbus"; - (*pDeviceJson)[ProjectFileDefinitions::cIdJsonKey] = static_cast(pDeviceSettings->deviceId); (*pDeviceJson)[ProjectFileDefinitions::cConnectionIdTag] = static_cast(connectionId); (*pDeviceJson)[ProjectFileDefinitions::cSlaveIdTag] = static_cast(slaveId); @@ -426,7 +428,9 @@ GeneralError ProjectFileXmlParser::parseLogToFile(const QDomElement& element, Lo { if (child.tagName() == ProjectFileDefinitions::cFilenameTag) { - QFileInfo fileInfo = QFileInfo(child.text()); + QFileInfo fileInfo = (_projectBaseDir.isEmpty() || QFileInfo(child.text()).isAbsolute()) + ? QFileInfo(child.text()) + : QFileInfo(QDir(_projectBaseDir), child.text()); bool bValid = true; if (!fileInfo.isFile()) @@ -516,6 +520,11 @@ GeneralError ProjectFileXmlParser::parseRegisterTag(const QDomElement& element, { pRegisterSettings->valueAxis = axis; } + else + { + parseErr.reportError(QString("Value axis (%1) is not a valid number").arg(child.text())); + break; + } } else if (child.tagName() == ProjectFileDefinitions::cExpressionTag) { @@ -834,11 +843,15 @@ void ProjectFileXmlParser::buildAdapterSettings(const QJsonArray& connectionsArr adapterSettings.settings = settingsObj; pGeneralSettings->adapterList.append(adapterSettings); + const quint32 newAdapterIndex = static_cast(pGeneralSettings->adapterList.size() - 1); - /* Set adapterId on all devices to point to adapter index 0 */ + /* Assign adapter index and type to devices that have not yet been linked to an adapter */ for (int i = 0; i < pGeneralSettings->deviceSettings.size(); i++) { - pGeneralSettings->deviceSettings[i].adapterId = 0; - pGeneralSettings->deviceSettings[i].adapterType = "modbus"; + if (pGeneralSettings->deviceSettings[i].adapterType.isEmpty()) + { + pGeneralSettings->deviceSettings[i].adapterId = newAdapterIndex; + pGeneralSettings->deviceSettings[i].adapterType = "modbus"; + } } } diff --git a/src/importexport/projectfilexmlparser.h b/src/importexport/projectfilexmlparser.h index 83451f41..d010206f 100644 --- a/src/importexport/projectfilexmlparser.h +++ b/src/importexport/projectfilexmlparser.h @@ -24,11 +24,16 @@ class ProjectFileXmlParser /*! * \brief Parse a legacy XML MBS project file into ProjectSettings. - * \param fileContent Raw file contents. - * \param pSettings Output settings structure. + * \param fileContent Raw file contents. + * \param pSettings Output settings structure. + * \param projectBaseDir Absolute path of the directory containing the project file. + * Used to resolve relative log-file paths; pass an empty string + * to skip directory-existence validation for relative paths. * \return GeneralError — result() is true on success. */ - GeneralError parseFile(const QString& fileContent, ProjectFileData::ProjectSettings* pSettings); + GeneralError parseFile(const QString& fileContent, + ProjectFileData::ProjectSettings* pSettings, + const QString& projectBaseDir = QString()); private: GeneralError parseModbusTag(const QDomElement& element, ProjectFileData::GeneralSettings* pGeneralSettings); @@ -60,6 +65,7 @@ class ProjectFileXmlParser QDomDocument _domDocument; quint32 _dataLevel; + QString _projectBaseDir; }; #endif // PROJECTFILEXMLPARSER_H diff --git a/tests/importexport/projectfilexmltestdata.cpp b/tests/importexport/projectfilexmltestdata.cpp index f37d11ee..4baf6377 100644 --- a/tests/importexport/projectfilexmltestdata.cpp +++ b/tests/importexport/projectfilexmltestdata.cpp @@ -274,4 +274,30 @@ QString ProjectFileXmlTestData::cValueAxis = QString( " \n" ); +QString ProjectFileXmlTestData::cValueAxisInvalid = QString( + " \n" + " \n" + " \n" + " \n" + " Data point \n" + " \n" + " notanumber \n" + " \n" + " \n" + " \n" +); + +QString ProjectFileXmlTestData::cLogFileRelativePath = QString( + " \n" + " \n" + " \n" + " \n" + " \n" + " subdir/output.csv \n" + " \n" + " \n" + " \n" + " \n" +); + // clang-format on diff --git a/tests/importexport/projectfilexmltestdata.h b/tests/importexport/projectfilexmltestdata.h index 8d2c3910..dbb6122a 100644 --- a/tests/importexport/projectfilexmltestdata.h +++ b/tests/importexport/projectfilexmltestdata.h @@ -19,6 +19,8 @@ class ProjectFileXmlTestData static QString cScaleMinMax; static QString cScaleWindowAuto; static QString cValueAxis; + static QString cValueAxisInvalid; + static QString cLogFileRelativePath; }; #endif // PROJECTFILEXMLTESTDATA_H diff --git a/tests/importexport/tst_projectfilexmlparser.cpp b/tests/importexport/tst_projectfilexmlparser.cpp index 5b0d78ee..570e1d0f 100644 --- a/tests/importexport/tst_projectfilexmlparser.cpp +++ b/tests/importexport/tst_projectfilexmlparser.cpp @@ -4,9 +4,11 @@ #include "importexport/projectfilexmlparser.h" #include "projectfilexmltestdata.h" +#include #include #include #include +#include #include using ProjectFileData::ProjectSettings; @@ -272,6 +274,42 @@ void TestProjectFileXmlParser::valueAxis() QCOMPARE(settings.scope.registerList[2].valueAxis, static_cast(0)); } +void TestProjectFileXmlParser::valueAxisInvalid() +{ + ProjectFileXmlParser parser; + ProjectSettings settings; + + GeneralError err = parser.parseFile(ProjectFileXmlTestData::cValueAxisInvalid, &settings); + QVERIFY(!err.result()); +} + +void TestProjectFileXmlParser::logFileRelativePath() +{ + /* Create a temporary project directory that contains "subdir/" so the relative path is valid */ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + QVERIFY(QDir(tempDir.path()).mkdir("subdir")); + + ProjectFileXmlParser parserWithBase; + ProjectSettings settingsWithBase; + GeneralError errWithBase = + parserWithBase.parseFile(ProjectFileXmlTestData::cLogFileRelativePath, &settingsWithBase, tempDir.path()); + QVERIFY(errWithBase.result()); + QVERIFY(settingsWithBase.general.logSettings.bLogToFileFile); + QVERIFY(!settingsWithBase.general.logSettings.logFile.isEmpty()); + + /* A different base dir without "subdir/" must reject the relative path */ + QTemporaryDir wrongDir; + QVERIFY(wrongDir.isValid()); + + ProjectFileXmlParser parserWrongBase; + ProjectSettings settingsWrongBase; + GeneralError errWrongBase = + parserWrongBase.parseFile(ProjectFileXmlTestData::cLogFileRelativePath, &settingsWrongBase, wrongDir.path()); + QVERIFY(errWrongBase.result()); + QVERIFY(!settingsWrongBase.general.logSettings.bLogToFileFile); +} + QTEST_MAIN(TestProjectFileXmlParser) #include "tst_projectfilexmlparser.moc" diff --git a/tests/importexport/tst_projectfilexmlparser.h b/tests/importexport/tst_projectfilexmlparser.h index 6ba542cb..55c887ee 100644 --- a/tests/importexport/tst_projectfilexmlparser.h +++ b/tests/importexport/tst_projectfilexmlparser.h @@ -28,6 +28,9 @@ private slots: void scaleWindowAuto(); void valueAxis(); + void valueAxisInvalid(); + + void logFileRelativePath(); }; #endif // TST_PROJECTFILEXMLPARSER_H