From b76f812b6e23e106324dcedca9b0bfb84fea3b0b Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 16:34:24 +0100 Subject: [PATCH 1/8] Reduce the usage scope for boost::property_tree --- Viewer/ecflowUI/src/Highlighter.cpp | 18 ++- Viewer/ecflowUI/src/InfoPanelHandler.cpp | 30 ++--- Viewer/ecflowUI/src/MainWindow.cpp | 1 - Viewer/ecflowUI/src/MainWindow.hpp | 1 - Viewer/ecflowUI/src/MenuHandler.cpp | 76 +++++------- Viewer/ecflowUI/src/NodePanel.cpp | 4 - Viewer/ecflowUI/src/NodePanel.hpp | 1 - Viewer/ecflowUI/src/ServerFilter.hpp | 2 - Viewer/ecflowUI/src/VConfig.cpp | 115 +++++++++--------- Viewer/ecflowUI/src/VFilter.hpp | 2 - Viewer/ecflowUI/src/VSettings.cpp | 20 ++- Viewer/libViewer/src/Palette.cpp | 24 ++-- .../src/ecflow/base/AuthorisationDetails.hpp | 1 - 13 files changed, 122 insertions(+), 173 deletions(-) diff --git a/Viewer/ecflowUI/src/Highlighter.cpp b/Viewer/ecflowUI/src/Highlighter.cpp index 48fa6f2bd..939307cb1 100644 --- a/Viewer/ecflowUI/src/Highlighter.cpp +++ b/Viewer/ecflowUI/src/Highlighter.cpp @@ -87,30 +87,28 @@ void Highlighter::load(QString id) { return; } - ptree::const_assoc_iterator itTop = pt.find(id.toStdString()); - if (itTop == pt.not_found()) { + auto itTop = pt.find(id.toStdString()); + if (itTop == pt.not_found()) { // Not found! return; } - // For each parameter - for (ptree::const_iterator itRule = itTop->second.begin(); itRule != itTop->second.end(); ++itRule) { + // For each Rule + for (auto& [ruleName, ruleValue] : itTop->second) { QString pattern; QTextCharFormat format; - ptree ptPar = itRule->second; - - if (auto itPar = ptPar.find("pattern"); itPar != ptPar.not_found()) { + if (auto itPar = ruleValue.find("pattern"); itPar != ruleValue.not_found()) { // Found! pattern = QString::fromStdString(itPar->second.get_value()); } - if (auto itPar = ptPar.find("colour"); itPar != ptPar.not_found()) { + if (auto itPar = ruleValue.find("colour"); itPar != ruleValue.not_found()) { // Found! format.setForeground(VProperty::toColour(itPar->second.get_value())); } - if (auto itPar = ptPar.find("bold"); itPar != ptPar.not_found()) { + if (auto itPar = ruleValue.find("bold"); itPar != ruleValue.not_found()) { // Found! if (itPar->second.get_value() == "true") { format.setFontWeight(QFont::Bold); } } - if (auto itPar = ptPar.find("italic"); itPar != ptPar.not_found()) { + if (auto itPar = ruleValue.find("italic"); itPar != ruleValue.not_found()) { // Found! if (itPar->second.get_value() == "true") { format.setFontItalic(true); } diff --git a/Viewer/ecflowUI/src/InfoPanelHandler.cpp b/Viewer/ecflowUI/src/InfoPanelHandler.cpp index 8d5561bbf..1cb518630 100644 --- a/Viewer/ecflowUI/src/InfoPanelHandler.cpp +++ b/Viewer/ecflowUI/src/InfoPanelHandler.cpp @@ -53,33 +53,29 @@ void InfoPanelHandler::init(const std::string& configFile) { } // iterate over the top level of the tree - for (ptree::const_iterator itTopLevel = pt.begin(); itTopLevel != pt.end(); ++itTopLevel) { - if (itTopLevel->first == "info_panel") { + for (auto& [topLevelName, topLevelValue] : pt) { + if (topLevelName == "info_panel") { UiLog().dbg() << "Panels:"; - ptree const& panelsPt = itTopLevel->second; - // iterate through all the panels - for (ptree::const_iterator itPanel = panelsPt.begin(); itPanel != panelsPt.end(); ++itPanel) { - ptree const& panelPt = itPanel->second; - - std::string cname = panelPt.get("name", ""); + for (auto& [panelName, panelValue] : topLevelValue) { + std::string cname = panelValue.get("name", ""); UiLog().dbg() << " " << cname; auto* def = new InfoPanelDef(cname); - def->setLabel(panelPt.get("label", "")); - def->setIcon(panelPt.get("icon", "")); - def->setDockIcon(panelPt.get("dock_icon", "")); - def->setShow(panelPt.get("show", "")); - def->setTooltip(panelPt.get("tooltip", "")); - def->setButtonTooltip(panelPt.get("button_tooltip", "")); + def->setLabel(panelValue.get("label", "")); + def->setIcon(panelValue.get("icon", "")); + def->setDockIcon(panelValue.get("dock_icon", "")); + def->setShow(panelValue.get("show", "")); + def->setTooltip(panelValue.get("tooltip", "")); + def->setButtonTooltip(panelValue.get("button_tooltip", "")); - std::string enabled = panelPt.get("enabled_for", ""); - std::string visible = panelPt.get("visible_for", ""); + std::string enabled = panelValue.get("enabled_for", ""); + std::string visible = panelValue.get("visible_for", ""); - if (panelPt.get("hidden", "") == "1") { + if (panelValue.get("hidden", "") == "1") { def->setHidden(true); } diff --git a/Viewer/ecflowUI/src/MainWindow.cpp b/Viewer/ecflowUI/src/MainWindow.cpp index e20079235..1b643ec66 100644 --- a/Viewer/ecflowUI/src/MainWindow.cpp +++ b/Viewer/ecflowUI/src/MainWindow.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include "AboutDialog.hpp" #include "ChangeNotify.hpp" diff --git a/Viewer/ecflowUI/src/MainWindow.hpp b/Viewer/ecflowUI/src/MainWindow.hpp index ad8650cf2..a20fb8b7c 100644 --- a/Viewer/ecflowUI/src/MainWindow.hpp +++ b/Viewer/ecflowUI/src/MainWindow.hpp @@ -13,7 +13,6 @@ #include #include -#include #include "VInfo.hpp" #include "ui_MainWindow.h" diff --git a/Viewer/ecflowUI/src/MenuHandler.cpp b/Viewer/ecflowUI/src/MenuHandler.cpp index b2d5dc5fb..760d8c242 100644 --- a/Viewer/ecflowUI/src/MenuHandler.cpp +++ b/Viewer/ecflowUI/src/MenuHandler.cpp @@ -76,69 +76,49 @@ bool MenuHandler::readMenuConfigFile(const std::string& configFile) { // iterate over the top level of the tree - for (ptree::const_iterator itTopLevel = pt.begin(); itTopLevel != pt.end(); ++itTopLevel) { - // parse the menu definitions? + for (auto& [nameTop, valueTop] : pt) { - if (itTopLevel->first == "menus") { + if (nameTop == "menus") { UiLog().dbg() << "Menus:"; - ptree const& menusDef = itTopLevel->second; - // iterate through all the menus - for (ptree::const_iterator itMenus = menusDef.begin(); itMenus != menusDef.end(); ++itMenus) { - ptree const& menuDef = itMenus->second; + for (auto& [menuName, menuValue] : valueTop) { - std::string cname = menuDef.get("name", "NoName"); + std::string cname = menuValue.get("name", "NoName"); UiLog().dbg() << " " << cname; auto* menu = new Menu(cname); - // ptree const &menuModesDef = menuDef.get_child("modes"); - - // for (ptree::const_iterator itMenuModes = menuModesDef.begin(); itMenuModes != menuModesDef.end(); - // ++itMenuModes) - //{ - // std::cout << " +" << itMenuModes->second.data() << std::endl; - //} - - std::string parentMenuName = menuDef.get("parent", "None"); - - if (parentMenuName != "None") {} + std::string parentMenuName = menuValue.get("parent", "None"); addMenu(menu); // add to our list of available menus } } - - // parse the menu items? - - else if (itTopLevel->first == "menu_items") { + else if (nameTop == "menu_items") { UiLog().dbg() << "Menu items:"; - ptree const& itemsDef = itTopLevel->second; - - // iterate through all the items - - for (ptree::const_iterator itItems = itemsDef.begin(); itItems != itemsDef.end(); ++itItems) { - ptree const& ItemDef = itItems->second; - - std::string name = ItemDef.get("name", "NoName"); - std::string menuName = ItemDef.get("menu", "NoMenu"); - std::string command = ItemDef.get("command", "NoCommand"); - std::string type = ItemDef.get("type", "Command"); - std::string enabled = ItemDef.get("enabled_for", ""); - std::string visible = ItemDef.get("visible_for", ""); - std::string questFor = ItemDef.get("question_for", ""); - std::string question = ItemDef.get("question", ""); - std::string questionControl = ItemDef.get("question_control", ""); - std::string warning = ItemDef.get("warning", ""); - std::string handler = ItemDef.get("handler", ""); - std::string views = ItemDef.get("view", ""); - std::string icon = ItemDef.get("icon", ""); - std::string hidden = ItemDef.get("hidden", "false"); - std::string multiSelect = ItemDef.get("multi", "true"); - std::string statustip = ItemDef.get("status_tip", ""); - std::string shortcut = ItemDef.get("shortcut", ""); - std::string panelPopupControl = ItemDef.get("panel_control", ""); + // iterator through all menu_items + + for (auto& [itemName, itemValue] : valueTop) { + + std::string name = itemValue.get("name", "NoName"); + std::string menuName = itemValue.get("menu", "NoMenu"); + std::string command = itemValue.get("command", "NoCommand"); + std::string type = itemValue.get("type", "Command"); + std::string enabled = itemValue.get("enabled_for", ""); + std::string visible = itemValue.get("visible_for", ""); + std::string questFor = itemValue.get("question_for", ""); + std::string question = itemValue.get("question", ""); + std::string questionControl = itemValue.get("question_control", ""); + std::string warning = itemValue.get("warning", ""); + std::string handler = itemValue.get("handler", ""); + std::string views = itemValue.get("view", ""); + std::string icon = itemValue.get("icon", ""); + std::string hidden = itemValue.get("hidden", "false"); + std::string multiSelect = itemValue.get("multi", "true"); + std::string statustip = itemValue.get("status_tip", ""); + std::string shortcut = itemValue.get("shortcut", ""); + std::string panelPopupControl = itemValue.get("panel_control", ""); // std::cout << " " << name << " :" << menuName << std::endl; diff --git a/Viewer/ecflowUI/src/NodePanel.cpp b/Viewer/ecflowUI/src/NodePanel.cpp index 26d7c97ea..0d5d616d7 100644 --- a/Viewer/ecflowUI/src/NodePanel.cpp +++ b/Viewer/ecflowUI/src/NodePanel.cpp @@ -328,20 +328,16 @@ void NodePanel::writeSettings(VComboSettings* vs) { vs->put("currentTabId", currentIdx); for (int i = 0; i < count(); i++) { - // boost::property_tree::ptree ptTab; if (Dashboard* nw = nodeWidget(i)) { std::string id = NodePanel::tabSettingsId(i); vs->beginGroup(id); nw->writeSettings(vs); vs->endGroup(); - // pt.add_child("tab_"+ ecf::convert_to(i),ptTab); } } } void NodePanel::readSettings(VComboSettings* vs) { - using boost::property_tree::ptree; - auto cnt = vs->get("tabCount", 0); auto currentIndex = vs->get("currentTabId", -1); diff --git a/Viewer/ecflowUI/src/NodePanel.hpp b/Viewer/ecflowUI/src/NodePanel.hpp index bf29cb247..9cc7345ca 100644 --- a/Viewer/ecflowUI/src/NodePanel.hpp +++ b/Viewer/ecflowUI/src/NodePanel.hpp @@ -12,7 +12,6 @@ #define ecflow_viewer_NodePanel_HPP #include -#include #include "TabWidget.hpp" #include "VInfo.hpp" diff --git a/Viewer/ecflowUI/src/ServerFilter.hpp b/Viewer/ecflowUI/src/ServerFilter.hpp index 04abe1041..191c8486e 100644 --- a/Viewer/ecflowUI/src/ServerFilter.hpp +++ b/Viewer/ecflowUI/src/ServerFilter.hpp @@ -18,8 +18,6 @@ class VSettings; -#include - class ServerFilterObserver { public: virtual ~ServerFilterObserver() = default; diff --git a/Viewer/ecflowUI/src/VConfig.cpp b/Viewer/ecflowUI/src/VConfig.cpp index 24f2af910..9a3d219af 100644 --- a/Viewer/ecflowUI/src/VConfig.cpp +++ b/Viewer/ecflowUI/src/VConfig.cpp @@ -38,7 +38,7 @@ VConfig::VConfig() { } VConfig::~VConfig() { - for (auto& group : groups_) { + for (auto group : groups_) { delete group; } groups_.clear(); @@ -60,16 +60,16 @@ void VConfig::init(const std::string& parDirPath) { // The conf files have to be loaded in alphabetical order!! At least NotifyChange require it! // So we read the paths into a vector and sort it. - std::vector vec; - copy(fs::directory_iterator(parDir), fs::directory_iterator(), back_inserter(vec)); - std::sort(vec.begin(), vec.end()); + std::vector paths; + copy(fs::directory_iterator(parDir), fs::directory_iterator(), back_inserter(paths)); + std::sort(paths.begin(), paths.end()); // The paths are now in alphabetical order - for (auto it = vec.begin(); it != vec.end(); ++it) { - if (fs::is_regular_file(*it)) { - std::string name = it->filename().string(); + for (const auto& path : paths) { + if (fs::is_regular_file(path)) { + std::string name = path.filename().string(); if (name.find("_conf.json") != std::string::npos) { - loadInit(it->string()); + loadInit(path.string()); } } } @@ -106,22 +106,20 @@ void VConfig::loadInit(const std::string& parFile) { } // Loop over the groups - for (ptree::const_iterator itGr = pt.begin(); itGr != pt.end(); ++itGr) { - ptree ptGr = itGr->second; + for (auto& [groupName, groupValue] : pt) { // Get the group name and create it - std::string groupName = itGr->first; - auto* grProp = new VProperty(groupName); - groups_.push_back(grProp); + auto* groupProp = new VProperty(groupName); + groups_.push_back(groupProp); UiLog().dbg() << "VConfig::loadInit() read config group: " << groupName; // Load the property parameters. It will recursively add all the // children properties. - loadProperty(ptGr, grProp); + loadProperty(groupValue, groupProp); // Add the group we created to the registered configloader - VConfigLoader::process(groupName, grProp); + VConfigLoader::process(groupName, groupProp); } } @@ -131,24 +129,22 @@ void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* pro ptree::const_assoc_iterator itProp; // Loop over the possible properties - for (ptree::const_iterator it = pt.begin(); it != pt.end(); ++it) { - std::string name = it->first; - ptree ptProp = it->second; + for (auto& [propertyName, propertyValue] : pt) { #ifdef UI_CONFIG_LOAD_DEBUG - UiLog().dbg() << " VConfig::loadProperty() read item: " << name; + UiLog().dbg() << " VConfig::loadProperty() read item: " << propertyName; #endif // Default value - if (name == "default") { - auto val = ptProp.get_value(); + if (propertyName == "default") { + auto val = propertyValue.get_value(); prop->setDefaultValue(val); } // If it is just a key/value pair "line" - else if (name == "line" && ptProp.empty()) { - auto* chProp = new VProperty(name); + else if (propertyName == "line" && propertyValue.empty()) { + auto* chProp = new VProperty(propertyName); prop->addChild(chProp); - auto val = ptProp.get_value(); + auto val = propertyValue.get_value(); QString prefix = prop->param("prefix"); if (!prefix.isEmpty()) { @@ -171,8 +167,8 @@ void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* pro } } // If the property is a "line" (i.e. a line with additional parameters) - else if (prop->name() == "line" && name == "link") { - auto val = ptProp.get_value(); + else if (prop->name() == "line" && propertyName == "link") { + auto val = propertyValue.get_value(); #ifdef UI_CONFIG_LOAD_DEBUG UiLog().dbg() << " VConfig::loadProperty() line link: " << val; @@ -193,15 +189,15 @@ void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* pro // Here we only load the properties with // children (i.e. key/value pairs (like "line" etc above) // are ignored. - else if (!ptProp.empty()) { - auto* chProp = new VProperty(name); + else if (!propertyValue.empty()) { + auto* chProp = new VProperty(propertyName); prop->addChild(chProp); - loadProperty(ptProp, chProp); + loadProperty(propertyValue, chProp); chProp->adjustAfterLoad(); } else { - QString val = QString::fromStdString(ptProp.get_value()); - prop->setParam(QString::fromStdString(name), val); + QString val = QString::fromStdString(propertyValue.get_value()); + prop->setParam(QString::fromStdString(propertyName), val); } } } @@ -209,8 +205,8 @@ void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* pro VProperty* VConfig::find(const std::string& path) { VProperty* res = nullptr; - for (auto it = groups_.begin(); it != groups_.end(); ++it) { - VProperty* vGroup = *it; + for (auto it : groups_) { + VProperty* vGroup = it; res = vGroup->find(path); if (res) { return res; @@ -221,9 +217,9 @@ VProperty* VConfig::find(const std::string& path) { } VProperty* VConfig::group(const std::string& name) { - for (auto it = groups_.begin(); it != groups_.end(); ++it) { - if ((*it)->strName() == name) { - return *it; + for (auto it : groups_) { + if (it->strName() == name) { + return it; } } @@ -267,16 +263,16 @@ void VConfig::saveSettings(const std::string& parFile, VProperty* guiProp, VSett std::vector linkVec; guiProp->collectLinks(linkVec); - for (auto it = linkVec.begin(); it != linkVec.end(); ++it) { + for (auto it : linkVec) { if (global) { - if ((*it)->changed()) { - pt.put((*it)->path(), (*it)->valueAsStdString()); + if (it->changed()) { + pt.put((*it).path(), (*it).valueAsStdString()); } } else { - if (!(*it)->useMaster()) { - pt.put((*it)->path(), (*it)->valueAsStdString()); + if (!it->useMaster()) { + pt.put((*it).path(), (*it).valueAsStdString()); } } } @@ -284,8 +280,8 @@ void VConfig::saveSettings(const std::string& parFile, VProperty* guiProp, VSett // Add settings stored in VSettings if (vs) { // Loop over the possible properties - for (ptree::const_iterator it = vs->propertyTree().begin(); it != vs->propertyTree().end(); ++it) { - pt.add_child(it->first, it->second); + for (auto& it : vs->propertyTree()) { + pt.add_child(it.first, it.second); } } @@ -326,15 +322,16 @@ void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool return; } - for (auto it = linkVec.begin(); it != linkVec.end(); ++it) { - if (pt.get_child_optional((*it)->path()) != boost::none) { - auto val = pt.get((*it)->path()); + for (auto it : linkVec) { + const auto& path = it->path(); + if (auto found = pt.get_child_optional(path); found) { + auto val = found.value().get_value(); if (!global) { - (*it)->setUseMaster(false); + it->setUseMaster(false); } - (*it)->setValue(val); + it->setValue(val); } } @@ -344,11 +341,11 @@ void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool if (global) { std::string prevPath = "menu.access.nodeMenuMode"; std::string actPath = "server.menu.nodeMenuMode"; - if (pt.get_child_optional(prevPath) != boost::none) { - for (auto it = linkVec.begin(); it != linkVec.end(); ++it) { - if ((*it)->path() == actPath) { - auto val = pt.get(prevPath); - (*it)->setValue(val); + if (auto found = pt.get_child_optional(prevPath); found) { + for (auto it : linkVec) { + if (it->path() == actPath) { + auto val = found.value().get_value(); + it->setValue(val); break; } } @@ -360,13 +357,13 @@ void VConfig::loadImportedSettings(const boost::property_tree::ptree& pt, VPrope std::vector linkVec; guiProp->collectLinks(linkVec); - for (auto it = linkVec.begin(); it != linkVec.end(); ++it) { - if (pt.get_child_optional((*it)->path()) != boost::none) { - auto val = pt.get((*it)->path()); - (*it)->setValue(val); + for (auto it : linkVec) { + if (auto found = pt.get_child_optional(it->path()); found) { + auto val = found.value().get_value(); + it->setValue(val); } - else if ((*it)->master()) { - (*it)->setUseMaster(true); + else if (it->master()) { + it->setUseMaster(true); } } } diff --git a/Viewer/ecflowUI/src/VFilter.hpp b/Viewer/ecflowUI/src/VFilter.hpp index f16d6298e..31ade21c4 100644 --- a/Viewer/ecflowUI/src/VFilter.hpp +++ b/Viewer/ecflowUI/src/VFilter.hpp @@ -32,8 +32,6 @@ class VNode; class VSettings; class VTree; -#include - class VParamSet : public QObject { Q_OBJECT diff --git a/Viewer/ecflowUI/src/VSettings.cpp b/Viewer/ecflowUI/src/VSettings.cpp index 6c7f6ff13..05ec15e75 100644 --- a/Viewer/ecflowUI/src/VSettings.cpp +++ b/Viewer/ecflowUI/src/VSettings.cpp @@ -154,26 +154,24 @@ void VSettings::putAsBool(const std::string& key, bool val) { } void VSettings::get(const std::string& key, std::vector& val) { - boost::optional ptArray = pt_.get_child_optional(path_.path(key)); + auto ptArray = pt_.get_child_optional(path_.path(key)); if (!ptArray) { return; } - // boost::property_tree::ptree ptArray=it->second; - for (boost::property_tree::ptree::const_iterator it = ptArray.get().begin(); it != ptArray.get().end(); ++it) { - auto name = it->second.get_value(); - val.push_back(name); + for (auto& [_, value] : ptArray.value()) { + val.push_back(value.get_value()); } } void VSettings::get(const std::string& key, std::vector& val) { - boost::optional ptArray = pt_.get_child_optional(path_.path(key)); + auto ptArray = pt_.get_child_optional(path_.path(key)); if (!ptArray) { return; } - for (boost::property_tree::ptree::const_iterator it = ptArray.get().begin(); it != ptArray.get().end(); ++it) { - val.push_back(it->second.get_value()); + for (auto& [_, value] : ptArray.value()) { + val.push_back(value.get_value()); } } @@ -188,13 +186,13 @@ bool VSettings::getAsBool(const std::string& key, bool defaultVal) { // for getting a list of 'structs' void VSettings::get(const std::string& key, std::vector& val) { - boost::optional ptArray = pt_.get_child_optional(path_.path(key)); + auto ptArray = pt_.get_child_optional(path_.path(key)); if (!ptArray) { return; } - for (boost::property_tree::ptree::const_iterator it = ptArray.get().begin(); it != ptArray.get().end(); ++it) { - boost::property_tree::ptree child = it->second; + for (auto& it : ptArray.value()) { + auto child = it.second; VSettings vs(child); val.push_back(vs); } diff --git a/Viewer/libViewer/src/Palette.cpp b/Viewer/libViewer/src/Palette.cpp index f83212814..fc2405080 100644 --- a/Viewer/libViewer/src/Palette.cpp +++ b/Viewer/libViewer/src/Palette.cpp @@ -71,34 +71,26 @@ void Palette::load(const std::string& parFile) { QPalette palette = qApp->palette(); - for (ptree::const_iterator it = pt.begin(); it != pt.end(); ++it) { - std::string name = it->first; - ptree ptItem = it->second; - + for (auto& [topName, topValue] : pt) { QPalette::ColorGroup group = QPalette::Active; - if (name == "active") { + if (topName == "active") { group = QPalette::Active; } - else if (name == "inactive") { + else if (topName == "inactive") { group = QPalette::Inactive; } - else if (name == "disabled") { + else if (topName == "disabled") { group = QPalette::Disabled; } else { UserMessage::message( - UserMessage::ERROR, true, std::string("Error! Palette::load() unable to identify group: " + name)); + UserMessage::ERROR, true, std::string("Error! Palette::load() unable to identify group: " + topName)); continue; } - for (ptree::const_iterator itItem = ptItem.begin(); itItem != ptItem.end(); ++itItem) { - std::string role = itItem->first; - auto val = itItem->second.get_value(); - - QMap::const_iterator itP = paletteId.find(role); - if (itP != paletteId.end()) { - QColor col = toColour(val); - if (col.isValid()) { + for (auto& [itemName, itemValue] : topValue) { + if (auto itP = paletteId.find(itemName); itP != paletteId.end()) { + if (auto col = toColour(itemValue.get_value()); col.isValid()) { palette.setColor(group, itP.value(), col); } } diff --git a/libs/base/src/ecflow/base/AuthorisationDetails.hpp b/libs/base/src/ecflow/base/AuthorisationDetails.hpp index 514244686..4ff956942 100644 --- a/libs/base/src/ecflow/base/AuthorisationDetails.hpp +++ b/libs/base/src/ecflow/base/AuthorisationDetails.hpp @@ -12,7 +12,6 @@ #define ecflow_base_AuthorisationDetails_HPP #include -#include #include "ecflow/base/AbstractServer.hpp" #include "ecflow/base/Authorisation.hpp" From 3d2b32b2b0c2be494fbf438c373361f21df837d3 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 17:08:59 +0100 Subject: [PATCH 2/8] Avoid deep-copy of subtree on iterator dereference --- Viewer/ecflowUI/src/Highlighter.cpp | 20 +- Viewer/ecflowUI/src/InfoPanelHandler.cpp | 11 +- Viewer/ecflowUI/src/MenuHandler.cpp | 8 +- Viewer/ecflowUI/src/VConfig.cpp | 35 +- Viewer/ecflowUI/src/VConfig.hpp | 8 +- Viewer/ecflowUI/src/VServerSettings.cpp | 6 +- Viewer/ecflowUI/src/VSettings.cpp | 26 +- Viewer/ecflowUI/src/VSettings.hpp | 9 +- Viewer/libViewer/src/Palette.cpp | 10 +- libs/CMakeLists.txt | 2 + libs/core/CMakeLists.txt | 1 + libs/core/src/ecflow/core/PTree.cpp | 380 +++++ libs/core/src/ecflow/core/PTree.hpp | 566 ++++++++ libs/core/test/TestPTree.cpp | 1536 ++++++++++++++++++++ libs/core/test/data/PTree/highlighter.json | 22 + libs/core/test/data/PTree/menu.json | 48 + libs/core/test/data/PTree/palette.json | 23 + libs/core/test/data/PTree/settings.json | 50 + libs/core/test/data/PTree/simple.json | 12 + 19 files changed, 2700 insertions(+), 73 deletions(-) create mode 100644 libs/core/src/ecflow/core/PTree.cpp create mode 100644 libs/core/src/ecflow/core/PTree.hpp create mode 100644 libs/core/test/TestPTree.cpp create mode 100644 libs/core/test/data/PTree/highlighter.json create mode 100644 libs/core/test/data/PTree/menu.json create mode 100644 libs/core/test/data/PTree/palette.json create mode 100644 libs/core/test/data/PTree/settings.json create mode 100644 libs/core/test/data/PTree/simple.json diff --git a/Viewer/ecflowUI/src/Highlighter.cpp b/Viewer/ecflowUI/src/Highlighter.cpp index 939307cb1..da8724733 100644 --- a/Viewer/ecflowUI/src/Highlighter.cpp +++ b/Viewer/ecflowUI/src/Highlighter.cpp @@ -15,11 +15,11 @@ #include #include #include -#include -#include +#include "UiLog.hpp" #include "UserMessage.hpp" #include "VParam.hpp" +#include "ecflow/core/PTree.hpp" std::string Highlighter::parFile_; @@ -72,13 +72,13 @@ void Highlighter::init(const std::string& parFile) { void Highlighter::load(QString id) { // Parse param file using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + + ecf::PTree pt; try { read_json(parFile_, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); UserMessage::message(UserMessage::ERROR, true, @@ -88,7 +88,7 @@ void Highlighter::load(QString id) { } auto itTop = pt.find(id.toStdString()); - if (itTop == pt.not_found()) { // Not found! + if (itTop == pt.end()) { // Not found! return; } @@ -97,18 +97,18 @@ void Highlighter::load(QString id) { QString pattern; QTextCharFormat format; - if (auto itPar = ruleValue.find("pattern"); itPar != ruleValue.not_found()) { // Found! + if (auto itPar = ruleValue.find("pattern"); itPar != ruleValue.end()) { // Found! pattern = QString::fromStdString(itPar->second.get_value()); } - if (auto itPar = ruleValue.find("colour"); itPar != ruleValue.not_found()) { // Found! + if (auto itPar = ruleValue.find("colour"); itPar != ruleValue.end()) { // Found! format.setForeground(VProperty::toColour(itPar->second.get_value())); } - if (auto itPar = ruleValue.find("bold"); itPar != ruleValue.not_found()) { // Found! + if (auto itPar = ruleValue.find("bold"); itPar != ruleValue.end()) { // Found! if (itPar->second.get_value() == "true") { format.setFontWeight(QFont::Bold); } } - if (auto itPar = ruleValue.find("italic"); itPar != ruleValue.not_found()) { // Found! + if (auto itPar = ruleValue.find("italic"); itPar != ruleValue.end()) { // Found! if (itPar->second.get_value() == "true") { format.setFontItalic(true); } diff --git a/Viewer/ecflowUI/src/InfoPanelHandler.cpp b/Viewer/ecflowUI/src/InfoPanelHandler.cpp index 1cb518630..08adf33e6 100644 --- a/Viewer/ecflowUI/src/InfoPanelHandler.cpp +++ b/Viewer/ecflowUI/src/InfoPanelHandler.cpp @@ -10,12 +10,10 @@ #include "InfoPanelHandler.hpp" -#include -#include - #include "NodeExpression.hpp" #include "UiLog.hpp" #include "UserMessage.hpp" +#include "ecflow/core/PTree.hpp" InfoPanelHandler* InfoPanelHandler::instance_ = nullptr; @@ -39,13 +37,12 @@ InfoPanelHandler* InfoPanelHandler::instance() { void InfoPanelHandler::init(const std::string& configFile) { // parse the response using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; try { read_json(configFile, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); UserMessage::message( UserMessage::ERROR, true, std::string("Error, unable to parse JSON menu file : " + errorMessage)); @@ -58,7 +55,7 @@ void InfoPanelHandler::init(const std::string& configFile) { UiLog().dbg() << "Panels:"; // iterate through all the panels - for (auto& [panelName, panelValue] : topLevelValue) { + for (auto& [_, panelValue] : topLevelValue) { std::string cname = panelValue.get("name", ""); UiLog().dbg() << " " << cname; diff --git a/Viewer/ecflowUI/src/MenuHandler.cpp b/Viewer/ecflowUI/src/MenuHandler.cpp index 760d8c242..032d58048 100644 --- a/Viewer/ecflowUI/src/MenuHandler.cpp +++ b/Viewer/ecflowUI/src/MenuHandler.cpp @@ -20,8 +20,6 @@ #include #include #include -#include -#include #if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) #include @@ -40,6 +38,7 @@ #include "VProperty.hpp" #include "ViewerUtil.hpp" #include "ecflow/core/Str.hpp" +#include "ecflow/core/PTree.hpp" int MenuItem::idCnt_ = 0; @@ -61,13 +60,12 @@ MenuHandler::MenuHandler() { bool MenuHandler::readMenuConfigFile(const std::string& configFile) { // parse the response using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; try { read_json(configFile, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); UserMessage::message( UserMessage::ERROR, true, std::string("Error, unable to parse JSON menu file : " + errorMessage)); diff --git a/Viewer/ecflowUI/src/VConfig.cpp b/Viewer/ecflowUI/src/VConfig.cpp index 9a3d219af..512e2b699 100644 --- a/Viewer/ecflowUI/src/VConfig.cpp +++ b/Viewer/ecflowUI/src/VConfig.cpp @@ -10,8 +10,9 @@ #include "VConfig.hpp" +#include + #include -#include #include "DirectoryHandler.hpp" #include "SessionHandler.hpp" @@ -21,6 +22,7 @@ #include "VProperty.hpp" #include "VSettings.hpp" #include "ecflow/core/Filesystem.hpp" +#include "ecflow/core/PTree.hpp" #include "ecflow/core/Str.hpp" #include "ecflow/core/Version.hpp" @@ -89,13 +91,12 @@ void VConfig::init(const std::string& parDirPath) { void VConfig::loadInit(const std::string& parFile) { // Parse param file using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; try { read_json(parFile, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); UserMessage::message(UserMessage::ERROR, true, @@ -123,11 +124,7 @@ void VConfig::loadInit(const std::string& parFile) { } } -void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* prop) { - using boost::property_tree::ptree; - - ptree::const_assoc_iterator itProp; - +void VConfig::loadProperty(const ecf::PTree& pt, VProperty* prop) { // Loop over the possible properties for (auto& [propertyName, propertyValue] : pt) { @@ -256,8 +253,7 @@ void VConfig::saveSettings() { // Saves the settings per server that can be edited through the servers option gui void VConfig::saveSettings(const std::string& parFile, VProperty* guiProp, VSettings* vs, bool global) { - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; // Get editable properties. We will operate on the links. std::vector linkVec; @@ -305,13 +301,12 @@ void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool guiProp->collectLinks(linkVec); // Parse file using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; try { read_json(parFile, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { if (fs::exists(parFile)) { std::string errorMessage = e.what(); UserMessage::message(UserMessage::ERROR, @@ -353,7 +348,7 @@ void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool } } -void VConfig::loadImportedSettings(const boost::property_tree::ptree& pt, VProperty* guiProp) { +void VConfig::loadImportedSettings(const ecf::PTree& pt, VProperty* guiProp) { std::vector linkVec; guiProp->collectLinks(linkVec); @@ -369,7 +364,7 @@ void VConfig::loadImportedSettings(const boost::property_tree::ptree& pt, VPrope } void VConfig::importSettings() { - boost::property_tree::ptree pt; + ecf::PTree pt; std::string globalRcFile(DirectoryHandler::concatenate(DirectoryHandler::rcDir(), "user.default.options")); if (readRcFile(globalRcFile, pt)) { @@ -379,7 +374,7 @@ void VConfig::importSettings() { } } -bool VConfig::readRcFile(const std::string& rcFile, boost::property_tree::ptree& pt) { +bool VConfig::readRcFile(const std::string& rcFile, ecf::PTree& pt) { std::ifstream in(rcFile.c_str()); if (!in.good()) { @@ -464,11 +459,11 @@ bool VConfig::readRcFile(const std::string& rcFile, boost::property_tree::ptree& hasValue = true; } else if (par[0] == "suites") { - boost::property_tree::ptree suites; - suites.push_back(std::make_pair(std::string{}, boost::property_tree::ptree(par[1]))); + ecf::PTree suites; + suites.put_child(std::string{}, ecf::PTree(par[1])); for (unsigned int j = 1; j < vec.size(); j++) { - suites.push_back(std::make_pair(std::string{}, boost::property_tree::ptree(vec.at(j)))); + suites.put_child(std::string{}, ecf::PTree(vec.at(j))); } pt.put_child("suite_filter.suites", suites); diff --git a/Viewer/ecflowUI/src/VConfig.hpp b/Viewer/ecflowUI/src/VConfig.hpp index 310bb8d7e..d3092bead 100644 --- a/Viewer/ecflowUI/src/VConfig.hpp +++ b/Viewer/ecflowUI/src/VConfig.hpp @@ -13,7 +13,7 @@ #include -#include +#include "ecflow/core/PTree.hpp" class VProperty; class VServerSettings; @@ -44,12 +44,12 @@ class VConfig { VConfig(); void loadInit(const std::string& parFile); - void loadProperty(const boost::property_tree::ptree& pt, VProperty* prop); + void loadProperty(const ecf::PTree& pt, VProperty* prop); void loadSettings(); void saveSettings(const std::string& parFile, VProperty* guiProp, VSettings* vs, bool); void loadSettings(const std::string& parFile, VProperty* guiProp, bool); - void loadImportedSettings(const boost::property_tree::ptree& pt, VProperty* guiProp); - bool readRcFile(const std::string& rcFile, boost::property_tree::ptree& pt); + void loadImportedSettings(const ecf::PTree& pt, VProperty* guiProp); + bool readRcFile(const std::string& rcFile, ecf::PTree& pt); VProperty* group(const std::string& name); diff --git a/Viewer/ecflowUI/src/VServerSettings.cpp b/Viewer/ecflowUI/src/VServerSettings.cpp index 3da2b0eb6..6d0a9bcb9 100644 --- a/Viewer/ecflowUI/src/VServerSettings.cpp +++ b/Viewer/ecflowUI/src/VServerSettings.cpp @@ -12,8 +12,6 @@ #include -#include - #include "DirectoryHandler.hpp" #include "ServerHandler.hpp" #include "ServerItem.hpp" @@ -26,6 +24,7 @@ #include "VProperty.hpp" #include "VSettings.hpp" #include "ecflow/core/Filesystem.hpp" +#include "ecflow/core/PTree.hpp" std::map VServerSettings::notifyIds_; std::map VServerSettings::parNames_; @@ -213,8 +212,7 @@ void VServerSettings::importRcFiles() { std::string rcFile(DirectoryHandler::concatenate(DirectoryHandler::rcDir(), name + ".options")); - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; if (VConfig::instance()->readRcFile(rcFile, pt)) { std::string jsonName = cs->serverFile(name); diff --git a/Viewer/ecflowUI/src/VSettings.cpp b/Viewer/ecflowUI/src/VSettings.cpp index 05ec15e75..b5674261f 100644 --- a/Viewer/ecflowUI/src/VSettings.cpp +++ b/Viewer/ecflowUI/src/VSettings.cpp @@ -12,8 +12,6 @@ #include -#include - #include "DirectoryHandler.hpp" #include "UiLog.hpp" #include "UserMessage.hpp" @@ -61,7 +59,7 @@ VSettings::VSettings(const std::string& file) : file_(file) { } -VSettings::VSettings(boost::property_tree::ptree pt) +VSettings::VSettings(ecf::PTree pt) : pt_(pt) { } @@ -78,18 +76,18 @@ bool VSettings::fileExists() const { } bool VSettings::contains(const std::string& key) { - return (pt_.get_child_optional(path_.path(key)) != boost::none); + return pt_.contains(path_.path(key)); } bool VSettings::containsFullPath(const std::string& key) { - return (pt_.get_child_optional(key) != boost::none); + return pt_.contains(key); } bool VSettings::read(bool showPopupOnError, const std::string& extraMessage) { try { - boost::property_tree::json_parser::read_json(file_, pt_); + read_json(file_, pt_); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); if (!DirectoryHandler::isFirstStartUp()) { std::string m = "Unable to parse config file : " + errorMessage; @@ -123,30 +121,30 @@ void VSettings::put(const std::string& key, const std::string& val) { } void VSettings::put(const std::string& key, const std::vector& val) { - boost::property_tree::ptree array; + ecf::PTree array; for (const auto& it : val) { - array.push_back(std::make_pair("", boost::property_tree::ptree(it))); + array.push_back_array_element(ecf::PTree(it)); } pt_.put_child(path_.path(key), array); } void VSettings::put(const std::string& key, const std::vector& val) { - boost::property_tree::ptree array; + ecf::PTree array; for (int it : val) { std::stringstream ss; ss << it; - array.push_back(std::make_pair("", boost::property_tree::ptree(ss.str()))); + array.push_back_array_element(ecf::PTree(ss.str())); } pt_.put_child(path_.path(key), array); } // for adding a list of 'structs' void VSettings::put(const std::string& key, const std::vector& val) { - boost::property_tree::ptree array; + ecf::PTree array; for (const auto& it : val) { - array.push_back(std::make_pair("", it.pt_)); + array.push_back_array_element(it.pt_); } - pt_.put_child(path_.path(key), array); + pt_.add_child(path_.path(key), array); } void VSettings::putAsBool(const std::string& key, bool val) { diff --git a/Viewer/ecflowUI/src/VSettings.hpp b/Viewer/ecflowUI/src/VSettings.hpp index f4d45d214..bbcd4ab79 100644 --- a/Viewer/ecflowUI/src/VSettings.hpp +++ b/Viewer/ecflowUI/src/VSettings.hpp @@ -14,7 +14,8 @@ #include #include -#include + +#include "ecflow/core/PTree.hpp" class VSettingsPath { public: @@ -34,7 +35,7 @@ class VSettingsPath { class VSettings { public: explicit VSettings(const std::string& file); - explicit VSettings(boost::property_tree::ptree pt); + explicit VSettings(ecf::PTree pt); virtual ~VSettings() = default; // bool read(const std::string &fs); @@ -68,10 +69,10 @@ class VSettings { bool getAsBool(const std::string& key, bool defaultVal); void get(const std::string& key, std::vector& val); - const boost::property_tree::ptree& propertyTree() const { return pt_; } + const ecf::PTree& propertyTree() const { return pt_; } protected: - boost::property_tree::ptree pt_; + ecf::PTree pt_; VSettingsPath path_; std::string file_; }; diff --git a/Viewer/libViewer/src/Palette.cpp b/Viewer/libViewer/src/Palette.cpp index fc2405080..24db1ee75 100644 --- a/Viewer/libViewer/src/Palette.cpp +++ b/Viewer/libViewer/src/Palette.cpp @@ -22,8 +22,9 @@ #endif #include #include -#include -#include + +#include "UiLog.hpp" +#include "ecflow/core/PTree.hpp" static QMap paletteId; @@ -54,13 +55,12 @@ void Palette::load(const std::string& parFile) { } // Parse param file using the boost JSON property tree parser - using boost::property_tree::ptree; - ptree pt; + ecf::PTree pt; try { read_json(parFile, pt); } - catch (const boost::property_tree::json_parser::json_parser_error& e) { + catch (const ecf::PTreeParseError& e) { std::string errorMessage = e.what(); UserMessage::message(UserMessage::ERROR, true, diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 1c6ba88b3..f89761aaf 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -277,6 +277,7 @@ set(srcs core/src/ecflow/core/PasswordEncryption.hpp core/src/ecflow/core/Pid.hpp core/src/ecflow/core/PrintStyle.hpp + core/src/ecflow/core/PTree.hpp core/src/ecflow/core/Result.hpp core/src/ecflow/core/SState.hpp core/src/ecflow/core/Serialization.hpp @@ -319,6 +320,7 @@ set(srcs core/src/ecflow/core/PasswdFile.cpp core/src/ecflow/core/Pid.cpp core/src/ecflow/core/PrintStyle.cpp + core/src/ecflow/core/PTree.cpp core/src/ecflow/core/SState.cpp core/src/ecflow/core/Str.cpp core/src/ecflow/core/StringSplitter.cpp diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 202338f03..14d6162da 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -61,6 +61,7 @@ set(test_srcs test/TestPasswdFile.cpp test/TestPasswordEncryption.cpp test/TestPerfTimer.cpp + test/TestPTree.cpp test/TestRealCalendar.cpp test/TestResources.cpp test/TestResult.cpp diff --git a/libs/core/src/ecflow/core/PTree.cpp b/libs/core/src/ecflow/core/PTree.cpp new file mode 100644 index 000000000..0d21572ec --- /dev/null +++ b/libs/core/src/ecflow/core/PTree.cpp @@ -0,0 +1,380 @@ +/* + * Copyright 2009- ECMWF. + * + * This software is licensed under the terms of the Apache Licence version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation + * nor does it submit to any jurisdiction. + */ + +#include "ecflow/core/PTree.hpp" + +#include +#include + +#include + +namespace ecf { + +namespace detail { + +std::vector split_path(std::string_view path) { + std::vector parts; + if (path.empty()) { + return parts; + } + + std::string_view sv = path; + while (!sv.empty()) { + auto dot = sv.find('.'); + if (dot == std::string_view::npos) { + parts.emplace_back(sv); + break; + } + parts.emplace_back(sv.substr(0, dot)); + sv.remove_prefix(dot + 1); + } + return parts; +} + +} // namespace detail + +const PTree* PTree::navigate(std::string_view path) const noexcept { + const PTree* cur = this; + for (const auto& seg : detail::split_path(path)) { + if (cur->kind_ != Kind::Children) { + return nullptr; + } + bool found = false; + for (const auto& child : cur->children_) { + if (child.name_ == seg) { + cur = &child; + found = true; + break; + } + } + if (!found) { + return nullptr; + } + } + return cur; +} + +PTree& PTree::navigate_or_create(std::string_view path) { + PTree* cur = this; + for (const auto& seg : detail::split_path(path)) { + cur = &cur->get_or_create_child(seg); + } + return *cur; +} + +PTree& PTree::get_or_create_child(const std::string& key) { + if (kind_ != Kind::Children) { + kind_ = Kind::Children; + arr_ = false; + str_.clear(); + int_ = 0; + children_.clear(); + } + // Linear search — first match wins (put/navigate semantics). + for (auto& child : children_) { + if (child.name_ == key) { + return child; + } + } + // Not found: append a new null child with the requested name. + children_.emplace_back(); + children_.back().name_ = key; + return children_.back(); +} + +void PTree::copy_value_from(PTree src) { + kind_ = src.kind_; + str_ = std::move(src.str_); + int_ = src.int_; + arr_ = src.arr_; + children_ = std::move(src.children_); +} + +std::string PTree::get(std::string_view path, const char* dv) const { + return get(path, std::string(dv)); +} + +std::string PTree::get(std::string_view path, std::string dv) const { + return get(path, std::move(dv)); +} + +bool PTree::contains(std::string_view path) const noexcept { + return navigate(path) != nullptr; +} + +std::optional PTree::get_child_optional(std::string_view path) const { + const PTree* node = navigate(path); + if (!node) { + return std::nullopt; + } + return *node; +} + +PTree::const_iterator_t PTree::find(std::string_view key) const { + if (kind_ == Kind::Children) { + for (auto it = children_.cbegin(); it != children_.cend(); ++it) { + if (it->name_ == key) { + return PTreeConstIterator(it); + } + } + } + return end(); +} + +static const PTree::children_t& sentinel_vec() { + // A atable empty vector is used as a sentinel so that begin() == end() + // for null/leaf nodes without undefined behaviour from cross-container comparisons. + static const PTree::children_t sv; + return sv; +} + +PTree::const_iterator_t PTree::begin() const { + return kind_ == Kind::Children ? PTreeConstIterator(children_.cbegin()) : PTreeConstIterator(sentinel_vec().cend()); +} + +PTree::const_iterator_t PTree::end() const { + return kind_ == Kind::Children ? PTreeConstIterator(children_.cend()) : PTreeConstIterator(sentinel_vec().cend()); +} + +void PTree::put(std::string_view path, std::string value) { + // Using copy_value_from so the target node's name_ is preserved even though we are replacing its value. + navigate_or_create(path).copy_value_from(PTree(std::move(value))); +} + +void PTree::put(std::string_view path, int value) { + navigate_or_create(path).copy_value_from(PTree(value)); +} + +void PTree::put_child(std::string_view path, PTree child) { + if (path.empty()) { + // When path is empty, we append as an anonymous array element. + push_back_array_element(std::move(child)); + return; + } + navigate_or_create(path).copy_value_from(std::move(child)); +} + +void PTree::add_child(std::string_view key, PTree child) { + // Always appends, allowing duplicate names (to maintain boost::property_tree semantics). + if (kind_ != Kind::Children) { + kind_ = Kind::Children; + arr_ = false; + str_.clear(); + int_ = 0; + children_.clear(); + } + child.name_ = std::string(key); + children_.push_back(std::move(child)); +} + +void PTree::push_back_array_element(PTree child) { + if (kind_ == Kind::Null) { + // Convert null to array on first call. + kind_ = Kind::Children; + arr_ = true; + } + else if (kind_ != Kind::Children || !arr_) { + throw PTreeInvalidStateError("PTree::push_back_array_element: " + "node is not an array (it already has named children)"); + } + child.name_ = ""; // array elements always have an empty name + children_.push_back(std::move(child)); +} + +// +// JSON I/O +// +// The JSON-parser/serializer is used exclusively in this translation unit. +// + +// +// *** JSON-Parsing *** +// +// The nlohmann's SAX interface delivers one event per token without ever merging duplicate keys. +// This allows building a PTree directly from the event stream, preserving every repeated key in insertion order. +// + +class PTreeSaxHandler : public nlohmann::json_sax { +public: + explicit PTreeSaxHandler(PTree& root) + : root_(root) {} + + bool null() override { + deliver(PTree{}); + return true; + } + + bool boolean(bool val) override { + deliver(PTree(val ? "true" : "false")); + return true; + } + + bool number_integer(number_integer_t val) override { + deliver(PTree(static_cast(val))); + return true; + } + + bool number_unsigned(number_unsigned_t val) override { + deliver(PTree(static_cast(val))); + return true; + } + + bool number_float(number_float_t /*val*/, const string_t& s) override { + // This stores floats as their string representation to avoid precision loss + // and to keep get_value() well-behaved for callers. + deliver(PTree(s)); + return true; + } + + bool string(string_t& val) override { + deliver(PTree(std::move(val))); + return true; + } + + bool binary(binary_t& /*val*/) override { + deliver(PTree{}); + return true; + } + + bool start_object(std::size_t /*elements*/) override { + stack_.push_back({PTree{}, {}, /*is_array=*/false}); + return true; + } + + bool key(string_t& val) override { + stack_.back().pending_key = std::move(val); + return true; + } + + bool end_object() override { + finish_frame(); + return true; + } + + bool start_array(std::size_t /*elements*/) override { + stack_.push_back({PTree{}, {}, /*is_array=*/true}); + return true; + } + + bool end_array() override { + finish_frame(); + return true; + } + + bool parse_error(std::size_t /*pos*/, + const std::string& /*last_token*/, + const nlohmann::detail::exception& ex) override { + throw PTreeParseError(ex.what()); + } + +private: + struct Frame + { + PTree tree; + std::string pending_key; + bool is_array; + }; + + PTree& root_; + std::vector stack_; + + // Attach a completed value node to the appropriate parent. + void deliver(PTree val) { + if (stack_.empty()) { + // Top-level value (e.g. a file that is just a string or number). + root_ = std::move(val); + return; + } + auto& top = stack_.back(); + if (top.is_array) { + top.tree.push_back_array_element(std::move(val)); + } + else { + top.tree.add_child(top.pending_key, std::move(val)); + top.pending_key.clear(); + } + } + + // Pop the completed object/array frame and deliver it to its parent. + void finish_frame() { + auto frame = std::move(stack_.back()); + stack_.pop_back(); + deliver(std::move(frame.tree)); + } +}; + +void read_json(const std::string& filename, PTree& out) { + std::ifstream fs(filename); + if (!fs.is_open()) { + throw PTreeParseError("ecf::read_json: cannot open file: " + filename); + } + out = PTree{}; + PTreeSaxHandler handler(out); + try { + bool ok = nlohmann::json::sax_parse(fs, + &handler, + nlohmann::json::input_format_t::json, + /*strict=*/true, + /*ignore_comments=*/true); + if (!ok) { + throw PTreeParseError("ecf::read_json: parse failed for: " + filename); + } + } + catch (const PTreeParseError&) { + throw; + } + catch (const nlohmann::json::parse_error& e) { + throw PTreeParseError(std::string("ecf::read_json: parse error in '") + filename + "': " + e.what()); + } +} + +// +// *** JSON-Serialising *** +// +// Nodes with repeated names lose all but the last value for each name when +// converted to nlohmann (JSON objects cannot have duplicate keys). +// + +static nlohmann::ordered_json ptree_to_json(const PTree& pt) { + if (pt.is_a()) { + return nlohmann::ordered_json(pt.get_value()); + } + if (pt.is_a()) { + return nlohmann::ordered_json(pt.get_value()); + } + if (pt.is_array()) { + auto arr = nlohmann::ordered_json::array(); + for (const auto& [k, v] : pt) { + arr.push_back(ptree_to_json(v)); + } + return arr; + } + if (pt.is_object()) { + auto obj = nlohmann::ordered_json::object(); + for (const auto& [k, v] : pt) { + obj[k] = ptree_to_json(v); // last-wins for repeated names + } + return obj; + } + return nlohmann::ordered_json{}; // null +} + +void write_json(const std::string& filename, const PTree& in, int indent) { + std::ofstream fs(filename); + if (!fs.is_open()) { + throw PTreeParseError("ecf::write_json: cannot open file: " + filename); + } + fs << ptree_to_json(in).dump(indent) << '\n'; + if (!fs) { + throw PTreeParseError("ecf::write_json: write failed for: " + filename); + } +} + +} // namespace ecf diff --git a/libs/core/src/ecflow/core/PTree.hpp b/libs/core/src/ecflow/core/PTree.hpp new file mode 100644 index 000000000..5ff1e410c --- /dev/null +++ b/libs/core/src/ecflow/core/PTree.hpp @@ -0,0 +1,566 @@ +/* + * Copyright 2009- ECMWF. + * + * This software is licensed under the terms of the Apache Licence version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation + * nor does it submit to any jurisdiction. + */ + +#ifndef ecflow_core_PTree_HPP +#define ecflow_core_PTree_HPP + +#include +#include +#include +#include +#include +#include + +namespace ecf { + +/// +/// @brief Thrown when JSON content cannot be parsed. +/// +class PTreeParseError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +/// +/// @brief Thrown when a given path is absent. +/// +class PTreePathError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +/// +/// @brief Thrown when a value is not convertible to the expected type. +/// +class PTreeValueError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +/// +/// @brief Thrown when requesting a change in an invalid state +/// (e.g. adding array elements to a non-array PTree node). +/// +class PTreeInvalidStateError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class PTree; +class PTreeConstIterator; + +/// +/// @brief Parse a JSON file into a property tree. +/// +/// @param filename the name of the input file +/// @param out a property tree populated with the contents of the input file +/// @throws PTreeParseError if the file is not available or cannot be parsed as JSON +/// +void read_json(const std::string& filename, PTree& out); + +/// +/// @brief Serialize a property tree to JSON and write it to a file. +/// +/// @attention Writing a PTree with repeated keys WILL TRUNCATE the output +/// (JSON objects cannot have duplicate keys). +/// +/// This is acceptable because: +/// * Files with repeated keys (e.g. ecflowview_gui.json) are never written back by ecFlow. +/// * User settings written by write_json always/only have unique keys. +/// +/// @param filename the name of the output file +/// @param in a property tree to serialize +/// @param indent the number of spaces used for indentation +/// +void write_json(const std::string& filename, const PTree& in, int indent = 4); + +/// +/// @brief The PTree holds an in-memory property tree. +/// +/// * Design principles +/// +/// 1) Support repeated keys within a single node. +/// This is required to handle files such as ecflowview_gui.json, with content like: +/// "line": "a", +/// "line": "b", +/// "row" : { ... }, +/// "row" : { ... } +/// PTree preserves all entries as an ordered list. A SAX parser retains duplicate +/// keys, which technically makes the source "non-Standard JSON". +/// +/// 2) No JSON-parser implementation type is exposed in this header +/// +/// 3) Implemented as a replacement for boost::property_tree::ptree. +/// +/// * Node structure +/// +/// Each PTree node is a named value: +/// - name : the key used by this node's parent to identify it +/// (empty string "" for array elements and for the root node) +/// - value : one of +/// - null : default-constructed, unset +/// - string : leaf holding a std::string +/// - integer : leaf holding an int +/// - children : ordered list of child PTree nodes — supports repeated names +/// Children whose name is "" are treated as array elements; +/// children with non-empty names are treated as object fields. +/// Mixed usage is allowed but uncommon. +/// +/// * Iteration yields (std::string name, PTree value) pairs in insertion order, +/// for both object-like and array-like nodes ("" for the name when array-like). +/// +/// * Path separator: '.' (e.g. "server.notification.enabled"). +/// IMPORTANT: No escaping is performed — keys MUST NOT contain '.'. +/// +class PTree { +public: + using name_t = std::string; + using children_t = std::vector; + using const_iterator_t = PTreeConstIterator; + + enum class Kind { Null, String, Int, Children }; + + /// + /// @brief Default constructor, creates a null / empty node. + /// + PTree() = default; + + /// + /// @brief Create a leaf node holding a string value. + /// + /// @param value the string value + /// + explicit PTree(std::string value) + : kind_(Kind::String), + str_(std::move(value)) {} + + /// + /// @brief Create a leaf node holding a string value. + /// + /// @param value the string value + /// + explicit PTree(const char* value) + : kind_(Kind::String), + str_(value) {} + + /// + /// @brief Create a leaf node holding an integer value. + /// + /// @param value the string value + /// + explicit PTree(int value) + : kind_(Kind::Int), + int_(value) {} + + // PTree is Copyable & Movable. + PTree(const PTree&) = default; + PTree& operator=(const PTree&) = default; + PTree(PTree&&) = default; + PTree& operator=(PTree&&) = default; + + /// + /// @brief Verify if this node has no children. + /// + /// A leaf carrying a string/int value is considered "empty" because it + /// has no child nodes. + /// + /// @return `true` iff this node has no children. + /// + bool empty() const noexcept { return kind_ != Kind::Children ? true : children_.empty(); } + + /// + /// @brief Verify if this node is a leaf node (string or integer scalar). + /// + /// @return `true` iff this node holds a scalar value. + /// + bool is_leaf() const noexcept { return kind_ == Kind::String || kind_ == Kind::Int; } + + /// + /// @brief Verify if this node holds the specified Kind. + /// + /// @tparam K the Kind to check for + /// @return `true` iff this node holds the specified Kind + /// + template + bool is_a() const noexcept { + return kind_ == K; + } + + /// + /// @brief Verify if this node holds named children (object-like). + /// + /// @return `true` iff this node holds named children. + /// + bool is_object() const noexcept { return kind_ == Kind::Children && !arr_; } + + /// + /// @brief Verify if this node holds anonymous children (array-like). + /// + /// @return `true` iff this node holds anonymous children. + /// + bool is_array() const noexcept { return kind_ == Kind::Children && arr_; } + + /// + /// @brief Reset to null / empty. The node's own name is preserved. + /// + void clear() noexcept { + kind_ = Kind::Null; + str_.clear(); + int_ = 0; + arr_ = false; + children_.clear(); + // name_ is intentionally NOT cleared — it is the node's identity + // and is managed exclusively by the parent. + } + + /// + /// @brief Access the scalar value held directly by this node. + /// + /// @note Only specialisations for std::string, int, and bool are defined. + /// + /// @tparam T the type to convert the value to + /// @return the scalar value held by this node, converted to T + /// @throws PTreeValueError if conversion fails + /// + template + T get_value() const; + + /// + /// @brief Access the scalar at a dotted path, returning `default_value` if absent or if type conversion fails. + /// This is guaranteed never to throw. + /// + /// @tparam T the type to convert the value to + /// @param path the dotted path to look up (e.g. "server.notification.enabled") + /// @param default_value the value to return if the path is absent or if type conversion fails + /// @return the scalar value at the dotted path, converted to T, + /// or `default_value` if the path is absent or if type conversion fails + /// + template + T get(std::string_view path, T default_value) const noexcept; + + /// Convenience overloads for const char* / string literal defaults. + std::string get(std::string_view path, const char* dv) const; + std::string get(std::string_view path, std::string dv) const; + + /// + /// @brief Access the scalar at a dotted path, throwing PTreePathError if absent. + /// + /// @tparam T the type to convert the value to + /// @param path the dotted path to look up (e.g. "server.notification.enabled") + /// @return the scalar value at the dotted path, converted to T, + /// @throws PTreePathError if the path does not exist + /// @throws PTreeValueError if type conversion fails + /// + template + T get(std::string_view path) const; + + /// + /// @brief Access a child of the subtree at the dotted path. + /// + /// This effectively copies a subtree, if it exists + /// + /// @param path the dotted path to look up (e.g. "server.notification.enabled") + /// @return std::nullopt if the path does not exist, otherwise a copy of the subtree at the path + /// + std::optional get_child_optional(std::string_view path) const; + + /// + /// @brief Verify if child of the subtree at the dotted path exists. + /// + /// @param path the dotted path to look up (e.g. "server.notification.enabled") + /// @return `true` iff the dotted path exists (any node type). + /// + bool contains(std::string_view path) const noexcept; + + /// + /// @brief Find the first direct child whose key equals @p key. + /// + /// @param key the key to look up among direct children (not a dotted path) + /// @return a valid iterator if the key exists, otherwise end(). + /// + const_iterator_t find(std::string_view key) const; + + /// + /// @brief Set a scalar string at a dotted path, creating intermediate objects as needed. + /// Important: Last-wins if the final key already exists!!! + /// + /// @param path the dotted path to set (e.g. "server.notification.enabled") + /// @param value the scalar string to set + /// + void put(std::string_view path, std::string value); + + /// + /// @brief Set a scalar string at a dotted path, creating intermediate objects as needed. + /// Important: Last-wins if the final key already exists!!! + /// + /// @param path the dotted path to set (e.g. "server.notification.enabled") + /// @param value the scalar string to set + /// + void put(std::string_view path, const char* value) { put(path, std::string(value)); } + + /// + /// @brief Set a scalar integer at a dotted path, creating intermediate objects as needed. + /// Important: Last-wins if the final key already exists!!! + /// + /// @param path the dotted integer to set (e.g. "server.notification.enabled") + /// @param value the scalar string to set + /// + void put(std::string_view path, int value); + + /// + /// @brief Set a subtree at a dotted path, creating intermediate objects as needed. + /// + /// If path is empty, appends as an anonymous array element + /// — this is to handle the existing pattern `tree.put_child("", value)` used in readRcFile. + /// + /// @param path the dotted integer to set (e.g. "server.notification.enabled") + /// @param child the subtree + /// + void put_child(std::string_view path, PTree child); + + /// + /// @brief Append a direct subtree with the given key. + /// + /// @note Duplicate keys are allowed — all entries are preserved in insertion order. + /// + /// @param key the key associated to the child (not a dotted path) + /// @param child the subtree + /// + void add_child(std::string_view key, PTree child); + + /// + /// @brief Append a child as an anonymous array element (name = ""). + /// + /// Converts this null node to an array on first call. + /// + /// @param child the subtree to append as an array element + /// @throws PTreeInvalidStateError if the node already holds non-array children. + /// + void push_back_array_element(PTree child); + + /// + /// @brief The begin iterator over direct children in insertion order. + /// For object nodes: yields (key, subtree) pairs. + /// For array nodes: yields ("", element) pairs. + /// For null / leaf: empty range (begin() == end()). + /// + const_iterator_t begin() const; + + /// + /// @brief The end iterator over direct children. + /// + const_iterator_t end() const; + +private: + name_t name_{}; ///< This node's own name (key in the parent's children list). + + Kind kind_{Kind::Null}; ///< when kind_==Children: true → array ("" names), false → object + + // Object + std::string str_{}; + int int_{0}; + bool arr_{false}; + + // Array + children_t children_{}; ///< Each child carries its own name_ member. + + /// + /// @brief Traverse the dotted path and return a pointer to the target node. + /// + /// @param path the dotted path to navigate, e.g. "foo.bar.baz". Empty path returns *this. + /// @returns nullptr if any segment is missing or if a non-object node is encountered mid-path, otherwise the node + /// at the end of the path. + /// + const PTree* navigate(std::string_view path) const noexcept; + + /// + /// @brief Traverse the dotted path, creating intermediate nodes as needed, + /// and return a mutable reference to the target node. + /// + /// @param path the dotted path to navigate, e.g. "foo.bar.baz". + /// @returns the node at the end of the path. + /// + PTree& navigate_or_create(std::string_view path); + + /// + /// @brief Return a mutable reference to the first direct child with the given name, + /// creating a new null child if absent. Converts this node to an object if needed. + /// + /// @param key the key associated to the child (not a dotted path) + /// @return the node associated to the key, or a new null node if the key was not found among direct children + /// + PTree& get_or_create_child(const std::string& key); + + /// + /// @brief Copy value fields (kind, str, int, arr, children) from @p src into *this, + /// leaving name_ unchanged. + /// + /// This is the correct way to set the value of a node that already sits in a parent's + /// children_ vector, since direct operator= would also overwrite name_. + /// + void copy_value_from(PTree src); + + friend class PTreeConstIterator; +}; + +/// +/// @brief A forward const-iterator over the direct children of a PTree. +/// +/// Some technicallities: +/// +/// 1) PTree internally stores a std::vector (each child carrying its own name_), +/// which implies that the iterator cannot return a direct reference to a stored +/// std::pair. +/// 2) The iterator caches a lightweight view IteratorValue on first dereference, so +/// no deep copy of the child subtree is ever made, and the cache resets on each advance. +/// — this gives the same forward-iterator guarantee as a standard iterator: +/// a dereferenced value is valid until the iterator is incremented or destroyed. +/// +class PTreeConstIterator { +public: + /// + /// @brief Lightweight non-owning view of a (name, child) pair. + /// + /// Both members are references directly into the parent PTree's children vector. + /// + struct IteratorValue + { + const std::string& first; ///< Reference to the child's name. + const PTree& second; ///< Reference to the child PTree itself. + + IteratorValue(const std::string& f, const PTree& s) noexcept + : first(f), + second(s) {} + IteratorValue(const IteratorValue&) = default; + IteratorValue& operator=(const IteratorValue&) = delete; // reference members; no rebinding allowed + }; + + using value_type = IteratorValue; + using reference = const IteratorValue&; + using pointer = const IteratorValue*; + using difference_type = std::ptrdiff_t; + using iterator_category = std::forward_iterator_tag; + + reference operator*() const { + materialise(); + return *cache_; + } + pointer operator->() const { + materialise(); + return &*cache_; + } + + PTreeConstIterator& operator++() { + ++it_; + cache_.reset(); + return *this; + } + PTreeConstIterator operator++(int) { + auto t = *this; + ++(*this); + return t; + } + + bool operator==(const PTreeConstIterator& o) const { return it_ == o.it_; } + bool operator!=(const PTreeConstIterator& o) const { return it_ != o.it_; } + +private: + // inner_t points into a std::vector. + using inner_t = PTree::children_t::const_iterator; + + explicit PTreeConstIterator(inner_t it) + : it_(std::move(it)) {} + + /// + /// @brief Build (or reuse) the cached IteratorValue for the current position. + /// + /// Stores two references into the children vector, thus avoiding a copy of the subtree. + /// + void materialise() const { + if (!cache_) { + cache_.emplace(it_->name_, *it_); + } + } + + inner_t it_; + mutable std::optional cache_; + + friend class PTree; +}; + +template <> +inline std::string PTree::get_value() const { + switch (kind_) { + case Kind::String: + return str_; + case Kind::Int: + return std::to_string(int_); + case Kind::Null: + return {}; + case Kind::Children: + return {}; + } + return {}; +} + +template <> +inline int PTree::get_value() const { + if (kind_ == Kind::Int) { + return int_; + } + if (kind_ == Kind::String) { + try { + return std::stoi(str_); + } + catch (...) { + } + } + throw PTreeValueError("PTree::get_value: cannot convert to int"); +} + +template <> +inline bool PTree::get_value() const { + if (kind_ == Kind::String) { + if (str_ == "true" || str_ == "1") { + return true; + } + if (str_ == "false" || str_ == "0") { + return false; + } + } + throw PTreeValueError("PTree::get_value: cannot convert to bool"); +} + +template +T PTree::get(std::string_view path, T default_value) const noexcept { + const PTree* node = navigate(path); + if (!node) { + return default_value; + } + try { + return node->get_value(); + } + catch (...) { + return default_value; + } +} + +template +T PTree::get(std::string_view path) const { + const PTree* node = navigate(path); + if (!node) { + throw PTreePathError("PTree::get: path not found: " + std::string(path)); + } + return node->get_value(); +} + +} // namespace ecf + +#endif /* ecflow_core_PTree_HPP */ diff --git a/libs/core/test/TestPTree.cpp b/libs/core/test/TestPTree.cpp new file mode 100644 index 000000000..594901fb2 --- /dev/null +++ b/libs/core/test/TestPTree.cpp @@ -0,0 +1,1536 @@ +/* + * Copyright 2009- ECMWF. + * + * This software is licensed under the terms of the Apache Licence version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation + * nor does it submit to any jurisdiction. + */ + +#include +#include + +#include + +#include "ecflow/core/File.hpp" +#include "ecflow/core/Filesystem.hpp" +#include "ecflow/core/PTree.hpp" +#include "ecflow/test/scaffold/Naming.hpp" + +namespace { + +static std::string test_data_file(const std::string& name) { + std::string path = ecf::File::test_data("libs/core/test/data/PTree", "libs/core"); + return path + "/" + name; +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(U_Core) + +BOOST_AUTO_TEST_SUITE(T_PTree) + +BOOST_AUTO_TEST_CASE(ctor_default_constructor_produces_null_empty_node) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + BOOST_CHECK(t.empty()); + BOOST_CHECK(!t.is_leaf()); + BOOST_CHECK(!t.is_object()); + BOOST_CHECK(!t.is_array()); +} + +BOOST_AUTO_TEST_CASE(ctor_string_constructor_produces_leaf_node) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t("hello"); + BOOST_CHECK(t.is_leaf()); + BOOST_CHECK(t.empty()); + BOOST_CHECK(!t.is_object()); + BOOST_CHECK(!t.is_array()); + BOOST_CHECK(t.get_value() == "hello"); +} + +BOOST_AUTO_TEST_CASE(ctor_int_constructor_produces_leaf_node) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t(42); + BOOST_CHECK(t.is_leaf()); + BOOST_CHECK(t.empty()); + BOOST_CHECK(t.get_value() == 42); + // Also accessible as string + BOOST_CHECK(t.get_value() == "42"); +} + +BOOST_AUTO_TEST_CASE(ctor_cstring_constructor_produces_leaf_node) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t("literal"); + BOOST_CHECK(t.get_value() == "literal"); +} + +BOOST_AUTO_TEST_CASE(ctor_copy_constructor_does_not_share_content) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree a; + a.put("x", std::string("10")); + PTree b = a; // copy + BOOST_CHECK(b.get("x") == "10"); + + // Mutating b does not affect a + b.put("x", std::string("99")); + BOOST_CHECK(a.get("x") == "10"); + BOOST_CHECK(b.get("x") == "99"); +} + +BOOST_AUTO_TEST_CASE(c_tor_move_constructor_transfers_content) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree a; + a.put("k", std::string("v")); + PTree b = std::move(a); + BOOST_CHECK(b.get("k") == "v"); +} + +BOOST_AUTO_TEST_CASE(clear_resets_to_null) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("key", std::string("value")); + BOOST_CHECK(!t.empty()); + t.clear(); + BOOST_CHECK(t.empty()); + BOOST_CHECK(!t.contains("key")); +} + +BOOST_AUTO_TEST_CASE(object_node_is_not_a_leaf_and_not_empty_when_it_has_children) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("a", std::string("1")); + BOOST_CHECK(!t.empty()); + BOOST_CHECK(t.is_object()); + BOOST_CHECK(!t.is_leaf()); +} + +BOOST_AUTO_TEST_CASE(put_and_get_string_round_trip) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("name", std::string("Alice")); + BOOST_CHECK(t.get("name") == "Alice"); +} + +BOOST_AUTO_TEST_CASE(put_and_get_int_round_trip) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("count", 7); + BOOST_CHECK(t.get("count") == 7); +} + +BOOST_AUTO_TEST_CASE(put_cstring_and_get_string) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("key", "value"); + BOOST_CHECK(t.get("key") == "value"); +} + +BOOST_AUTO_TEST_CASE(put_overwrites_existing_value) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("x", std::string("first")); + t.put("x", std::string("second")); + BOOST_CHECK(t.get("x") == "second"); +} + +BOOST_AUTO_TEST_CASE(put_and_get_with_2_level_dotted_path) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("server.host", std::string("localhost")); + t.put("server.port", 3141); + BOOST_CHECK(t.get("server.host") == "localhost"); + BOOST_CHECK(t.get("server.port") == 3141); +} + +BOOST_AUTO_TEST_CASE(put_and_get_with_4_level_dotted_path) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("server.notification.aborted.enabled", std::string("true")); + BOOST_CHECK(t.get("server.notification.aborted.enabled") == "true"); +} + +BOOST_AUTO_TEST_CASE(intermediate_nodes_are_created_automatically) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("a.b.c.d", std::string("deep")); + BOOST_CHECK(t.contains("a")); + BOOST_CHECK(t.contains("a.b")); + BOOST_CHECK(t.contains("a.b.c")); + BOOST_CHECK(t.contains("a.b.c.d")); +} + +BOOST_AUTO_TEST_CASE(get_with_default_returns_default_when_path_is_absent) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + BOOST_CHECK(t.get("missing", "default") == "default"); + BOOST_CHECK(t.get("missing", -1) == -1); + BOOST_CHECK(t.get("missing", "fallback") == "fallback"); + BOOST_CHECK(t.get("missing", std::string("fallback")) == "fallback"); +} + +BOOST_AUTO_TEST_CASE(get_with_default_returns_default_when_intermediate_path_is_absent) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("a.b", std::string("x")); + // "a.c" never set + BOOST_CHECK(t.get("a.c", "nope") == "nope"); +} + +BOOST_AUTO_TEST_CASE(get_without_default_throws_when_path_is_absent) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + BOOST_CHECK_THROW(t.get("no.such.path"), ecf::PTreePathError); +} + +BOOST_AUTO_TEST_CASE(get_without_default_throws_via_JsonPathError_catchable_as_runtime_error) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + bool caught = false; + try { + t.get("missing"); + } + catch (const std::runtime_error&) { + caught = true; + } + BOOST_CHECK(caught); +} + +BOOST_AUTO_TEST_CASE(get_works_with_single_segment_path_without_dot) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("key", std::string("val")); + BOOST_CHECK(t.get("key", "") == "val"); +} + +BOOST_AUTO_TEST_CASE(get_int_coerces_string_encoded_integers) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("x", std::string("100")); // stored as JSON string + BOOST_CHECK(t.get("x", 0) == 100); +} + +BOOST_AUTO_TEST_CASE(get_string_coerces_int_typed_json_values) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("n", 42); + BOOST_CHECK(t.get("n", "") == "42"); +} + +BOOST_AUTO_TEST_CASE(value_string_on_int_node_returns_string_representation) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree leaf(99); + BOOST_CHECK(leaf.get_value() == "99"); +} + +BOOST_AUTO_TEST_CASE(value_int_on_string_with_int_node_works) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree leaf(std::string("42")); + BOOST_CHECK(leaf.get_value() == 42); +} + +BOOST_AUTO_TEST_CASE(get_with_default_does_not_throw_on_type_mismatch_returns_default) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("x", std::string("not_a_number")); + // Conversion string→int fails; should return default, not throw + BOOST_CHECK(t.get("x", -99) == -99); +} + +BOOST_AUTO_TEST_CASE(contains_returns_true_for_existing_paths) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("a.b.c", std::string("v")); + BOOST_CHECK(t.contains("a")); + BOOST_CHECK(t.contains("a.b")); + BOOST_CHECK(t.contains("a.b.c")); +} + +BOOST_AUTO_TEST_CASE(contains_returns_false_for_absent_paths) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("a.b", std::string("v")); + BOOST_CHECK(!t.contains("a.c")); + BOOST_CHECK(!t.contains("x")); + BOOST_CHECK(!t.contains("a.b.c.d")); +} + +BOOST_AUTO_TEST_CASE(get_child_optional_returns_value_for_existing_subtree) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("server.host", std::string("h1")); + t.put("server.port", 1234); + + auto opt = t.get_child_optional("server"); + BOOST_REQUIRE(opt.has_value()); + BOOST_CHECK(opt->get("host") == "h1"); + BOOST_CHECK(opt->get("port") == 1234); +} + +BOOST_AUTO_TEST_CASE(get_child_optional_returns_nullopt_for_absent_path) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + auto opt = t.get_child_optional("nonexistent"); + BOOST_CHECK(!opt.has_value()); +} + +BOOST_AUTO_TEST_CASE(get_child_optional_can_iterate_the_returned_subtree) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("section.a", std::string("1")); + t.put("section.b", std::string("2")); + + auto opt = t.get_child_optional("section"); + BOOST_REQUIRE(opt.has_value()); + + std::vector keys; + for (const auto& [k, v] : *opt) { + keys.push_back(k); + } + + BOOST_CHECK(keys.size() == 2); + BOOST_CHECK(keys[0] == "a"); + BOOST_CHECK(keys[1] == "b"); +} + +BOOST_AUTO_TEST_CASE(array_node_is_not_empty_after_push_back) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + arr.push_back_array_element(PTree("x")); + BOOST_CHECK(!arr.empty()); + BOOST_CHECK(arr.is_array()); + BOOST_CHECK(!arr.is_leaf()); +} + +BOOST_AUTO_TEST_CASE(push_back_array_element_converts_null_node_to_array) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + arr.push_back_array_element(PTree("a")); + BOOST_CHECK(arr.is_array()); +} + +BOOST_AUTO_TEST_CASE(push_back_array_element_appends_elements_in_order) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + arr.push_back_array_element(PTree("first")); + arr.push_back_array_element(PTree("second")); + arr.push_back_array_element(PTree("third")); + + std::vector result; + for (const auto& [k, v] : arr) { + BOOST_CHECK(k.empty()); // anonymous key + result.push_back(v.get_value()); + } + BOOST_REQUIRE(result.size() == 3); + BOOST_CHECK(result[0] == "first"); + BOOST_CHECK(result[1] == "second"); + BOOST_CHECK(result[2] == "third"); +} + +BOOST_AUTO_TEST_CASE(push_back_array_element_with_int_leaves) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + for (int i : {10, 20, 30}) { + arr.push_back_array_element(PTree(i)); + } + + std::vector result; + for (const auto& [k, v] : arr) { + result.push_back(v.get_value()); + } + + BOOST_REQUIRE(result.size() == 3); + BOOST_CHECK(result[0] == 10); + BOOST_CHECK(result[1] == 20); + BOOST_CHECK(result[2] == 30); +} + +BOOST_AUTO_TEST_CASE(vsettings_style_build_vector_string_as_array_and_retrieve_it) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + std::vector original = {"host1:3141", "host2:3141", "host3:3141"}; + + // Build + PTree root; + PTree arr; + for (const auto& s : original) { + arr.push_back_array_element(PTree(s)); + } + root.put_child("favourites", arr); + + // Retrieve + auto opt = root.get_child_optional("favourites"); + BOOST_REQUIRE(opt.has_value()); + std::vector retrieved; + for (const auto& [k, v] : *opt) { + retrieved.push_back(v.get_value()); + } + + BOOST_CHECK(retrieved == original); +} + +BOOST_AUTO_TEST_CASE(vsettings_style_build_vector_int_as_array_and_retrieve_it) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + std::vector original = {1, 2, 3, 5, 8, 13}; + + PTree root; + PTree arr; + for (int n : original) { + arr.push_back_array_element(PTree(n)); + } + root.put_child("counts", arr); + + auto opt = root.get_child_optional("counts"); + BOOST_REQUIRE(opt.has_value()); + std::vector retrieved; + for (const auto& [k, v] : *opt) { + retrieved.push_back(v.get_value()); + } + + BOOST_CHECK(retrieved == original); +} + +BOOST_AUTO_TEST_CASE(vsettings_style_build_array_of_object_subtrees) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + // Simulate: for each tab, build a sub-JsonTree and push it into an array. + struct TabInfo + { + std::string id, name; + }; + std::vector tabs = {{"0", "Ops"}, {"1", "Research"}, {"2", "Backup"}}; + + PTree root; + PTree arr; + for (const auto& tab : tabs) { + PTree sub; + sub.put("id", tab.id); + sub.put("name", tab.name); + arr.push_back_array_element(sub); + } + root.put_child("tabs", arr); + + // Retrieve + auto opt = root.get_child_optional("tabs"); + BOOST_REQUIRE(opt.has_value()); + + std::vector retrieved; + for (const auto& [k, v] : *opt) { + BOOST_CHECK(k.empty()); + retrieved.push_back({v.get("id"), v.get("name")}); + } + + BOOST_REQUIRE(retrieved.size() == 3); + BOOST_CHECK(retrieved[0].id == "0"); + BOOST_CHECK(retrieved[0].name == "Ops"); + BOOST_CHECK(retrieved[1].id == "1"); + BOOST_CHECK(retrieved[1].name == "Research"); + BOOST_CHECK(retrieved[2].id == "2"); + BOOST_CHECK(retrieved[2].name == "Backup"); +} + +BOOST_AUTO_TEST_CASE(empty_array_node_reports_empty_eq_true) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + arr.push_back_array_element(PTree("x")); + arr.push_back_array_element(PTree("y")); + BOOST_CHECK(!arr.empty()); + + PTree empty_arr; + BOOST_CHECK(empty_arr.empty()); +} + +BOOST_AUTO_TEST_CASE(put_child_with_empty_array_node_round_trips_correctly) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + PTree arr; // null, empty + arr.push_back_array_element(PTree("a")); + arr.push_back_array_element(PTree("b")); + root.put_child("list", arr); + + auto list_opt = root.get_child_optional("list"); + BOOST_REQUIRE(list_opt.has_value()); + int count = 0; + for ([[maybe_unused]] const auto& item : *list_opt) { + ++count; + } + BOOST_CHECK(count == 2); +} + +BOOST_AUTO_TEST_CASE(iterate_array_of_string_encoded_ints_array_coercion) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree arr; + arr.push_back_array_element(PTree(std::string("1"))); + arr.push_back_array_element(PTree(std::string("2"))); + arr.push_back_array_element(PTree(std::string("3"))); + + PTree root; + root.put_child("ids", arr); + + std::vector result; + auto ids_opt = root.get_child_optional("ids"); + BOOST_REQUIRE(ids_opt.has_value()); + for (const auto& [k, v] : *ids_opt) { + result.push_back(v.get_value()); + } + + BOOST_CHECK(result == std::vector({1, 2, 3})); +} + +BOOST_AUTO_TEST_CASE(push_back_array_element_throws_if_node_already_has_named_children) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put("key", std::string("val")); // now t is an object + BOOST_CHECK_THROW(t.push_back_array_element(PTree("oops")), ecf::PTreeInvalidStateError); +} + +BOOST_AUTO_TEST_CASE(object_iteration_yields_key_value_pairs) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("alpha", std::string("a")); + t.put("beta", std::string("b")); + t.put("gamma", std::string("c")); + + std::vector keys, values; + for (const auto& [k, v] : t) { + keys.push_back(k); + values.push_back(v.get_value()); + } + + BOOST_REQUIRE(keys.size() == 3); + BOOST_CHECK(keys[0] == "alpha"); + BOOST_CHECK(values[0] == "a"); + BOOST_CHECK(keys[1] == "beta"); + BOOST_CHECK(values[1] == "b"); + BOOST_CHECK(keys[2] == "gamma"); + BOOST_CHECK(values[2] == "c"); +} + +BOOST_AUTO_TEST_CASE(insertion_order_is_preserved_for_object_nodes) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + std::vector inserted = {"zebra", "apple", "mango", "kiwi", "banana"}; + for (const auto& k : inserted) { + t.put(k, std::string("v")); + } + + std::vector observed; + for (const auto& [k, v] : t) { + observed.push_back(k); + } + + BOOST_CHECK(observed == inserted); +} + +BOOST_AUTO_TEST_CASE(nested_object_iteration_VConfig_MenuHandler_pattern) { + ECF_NAME_THIS_TEST(); + + ecf::PTree root; + root.put("a.x", std::string("1")); + root.put("a.y", std::string("2")); + root.put("b.x", std::string("3")); + + std::vector> leaves; + for (const auto& [outer_key, outer_val] : root) { + for (const auto& [inner_key, inner_val] : outer_val) { + leaves.emplace_back(outer_key + "." + inner_key, inner_val.get_value()); + } + } + + BOOST_REQUIRE(leaves.size() == 3); + BOOST_CHECK(leaves[0].first == "a.x"); + BOOST_CHECK(leaves[0].second == "1"); + BOOST_CHECK(leaves[1].first == "a.y"); + BOOST_CHECK(leaves[1].second == "2"); + BOOST_CHECK(leaves[2].first == "b.x"); + BOOST_CHECK(leaves[2].second == "3"); +} + +BOOST_AUTO_TEST_CASE(two_level_iteration_Palette_pattern_outer_key_inner_key_leaf_value) { + ECF_NAME_THIS_TEST(); + + ecf::PTree palette; + palette.put("active.Window", std::string("#f0f0f0")); + palette.put("active.Highlight", std::string("#0078d4")); + palette.put("inactive.Window", std::string("#d8d8d8")); + palette.put("inactive.Highlight", std::string("#4a9fcf")); + + std::vector> entries; + for (const auto& [group, group_node] : palette) { + for (const auto& [role, role_node] : group_node) { + entries.emplace_back(group, role, role_node.get_value()); + } + } + + BOOST_REQUIRE(entries.size() == 4); + BOOST_CHECK(std::get<0>(entries[0]) == "active"); + BOOST_CHECK(std::get<1>(entries[0]) == "Window"); + BOOST_CHECK(std::get<2>(entries[0]) == "#f0f0f0"); + BOOST_CHECK(std::get<0>(entries[2]) == "inactive"); + BOOST_CHECK(std::get<1>(entries[2]) == "Window"); +} + +BOOST_AUTO_TEST_CASE(iterating_a_null_node_yields_nothing) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + int count = 0; + for ([[maybe_unused]] const auto& item : t) { + ++count; + } + BOOST_CHECK(count == 0); + BOOST_CHECK(t.begin() == t.end()); +} + +BOOST_AUTO_TEST_CASE(iterating_an_empty_object_yields_nothing) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree t; + t.put_child("sub", PTree()); + auto opt = t.get_child_optional("sub"); + BOOST_REQUIRE(opt.has_value()); + int count = 0; + for ([[maybe_unused]] const auto& item : *opt) { + ++count; + } + BOOST_CHECK(count == 0); +} + +BOOST_AUTO_TEST_CASE(find_returns_iterator_to_child_when_key_exists) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("rules.color", std::string("red")); + t.put("rules.bold", std::string("true")); + + auto it = t.find("rules"); + BOOST_CHECK(it != t.end()); + BOOST_CHECK(it->first == "rules"); + // The second is a subtree — iterate it + std::vector child_keys; + for (const auto& [k, v] : it->second) { + child_keys.push_back(k); + } + BOOST_CHECK(child_keys == std::vector({"color", "bold"})); +} + +BOOST_AUTO_TEST_CASE(find_returns_end_when_key_is_absent) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("a", std::string("1")); + auto it = t.find("nonexistent"); + BOOST_CHECK(it == t.end()); +} + +BOOST_AUTO_TEST_CASE(highlighter_style_pattern_find_nested_find_get_value) { + ECF_NAME_THIS_TEST(); + + ecf::PTree root; + root.put("ecfscript.0.pattern", std::string("^%[A-Z]+%")); + root.put("ecfscript.0.color", std::string("#0000cc")); + root.put("ecfscript.1.pattern", std::string("#.*$")); + root.put("ecfscript.1.color", std::string("#808080")); + + auto itTop = root.find("ecfscript"); + BOOST_REQUIRE(itTop != root.end()); + + std::vector patterns; + for (const auto& [rule_key, rule_node] : itTop->second) { + auto itPar = rule_node.find("pattern"); + if (itPar != rule_node.end()) { + patterns.push_back(itPar->second.get_value()); + } + } + BOOST_REQUIRE(patterns.size() == 2); + BOOST_CHECK(patterns[0] == "^%[A-Z]+%"); + BOOST_CHECK(patterns[1] == "#.*$"); +} + +BOOST_AUTO_TEST_CASE(put_child_replaces_existing_subtree) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + root.put("sub.a", std::string("old")); + + PTree replacement; + replacement.put("b", std::string("new")); + root.put_child("sub", replacement); + + BOOST_CHECK(!root.contains("sub.a")); + BOOST_CHECK(root.get("sub.b") == "new"); +} + +BOOST_AUTO_TEST_CASE(add_child_appends_overwrites_a_direct_child) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + root.put("x", std::string("1")); + + PTree extra; + extra.put("y", std::string("2")); + root.add_child("extra", extra); + + BOOST_CHECK(root.get("x") == "1"); + BOOST_CHECK(root.get("extra.y") == "2"); +} + +BOOST_AUTO_TEST_CASE(vconfig_savesettings_pattern_splice_subtree_children_into_parent) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree server_settings; + server_settings.put("host", std::string("prod.ecflow.int")); + server_settings.put("port", 3141); + server_settings.put("timeout", 30); + + PTree global; + global.put("version", std::string("7.0")); + for (const auto& [k, v] : server_settings) { + global.add_child(k, v); + } + + BOOST_CHECK(global.get("version") == "7.0"); + BOOST_CHECK(global.get("host") == "prod.ecflow.int"); + BOOST_CHECK(global.get("port") == 3141); + BOOST_CHECK(global.get("timeout") == 30); +} + +BOOST_AUTO_TEST_CASE(post_increment_iterator_works_correctly) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("a", std::string("1")); + t.put("b", std::string("2")); + + auto it = t.begin(); + auto first = it++; // post-increment: returns copy of old position + BOOST_CHECK(first->first == "a"); + BOOST_CHECK(it->first == "b"); +} + +BOOST_AUTO_TEST_CASE(iterator_with_view_has_correct_first_and_second) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + root.put("alpha", std::string("A")); + root.put("beta", std::string("B")); + root.put("gamma", std::string("C")); + + auto it = root.begin(); + BOOST_CHECK(it->first == "alpha"); + BOOST_CHECK(it->second.get_value() == "A"); + ++it; + BOOST_CHECK(it->first == "beta"); + BOOST_CHECK(it->second.get_value() == "B"); + ++it; + BOOST_CHECK(it->first == "gamma"); + BOOST_CHECK(it->second.get_value() == "C"); + ++it; + BOOST_CHECK(it == root.end()); +} + +BOOST_AUTO_TEST_CASE(iterator_dereference_returns_stable_reference_not_copy) { + ECF_NAME_THIS_TEST(); + + // Verification: + // (a) Correct key and value are accessible on a large subtree. + // (b) operator* on the same position returns the same address on every call + // (stable reference into children_, not a new allocation each time). + // (c) operator-> and operator* give consistent addresses. + + using ecf::PTree; + + // Build a child with many items so any copy would be clearly non-trivial. + PTree large_child; + for (int i = 0; i < 50; ++i) { + large_child.put("key" + std::to_string(i), std::to_string(i)); + } + + PTree root; + root.add_child("child", large_child); + + auto it = root.begin(); + + // (a) Values are correct + BOOST_CHECK(it->first == "child"); + BOOST_CHECK(it->second.contains("key0")); + BOOST_CHECK(it->second.get("key0") == "0"); + BOOST_CHECK(it->second.get("key49") == "49"); + + // (b) Same address on repeated dereference at the same position: + // a new deep copy would produce a different heap address each time. + const PTree* addr1 = &(*it).second; + const PTree* addr2 = &(*it).second; + BOOST_CHECK_EQUAL(addr1, addr2); + + // (c) operator-> and operator* are consistent + BOOST_CHECK_EQUAL(&it->second, &(*it).second); +} + +BOOST_AUTO_TEST_CASE(iterator_cache_is_reset_and_reflects_new_position_after_increment) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + root.put("x", std::string("1")); + root.put("y", std::string("2")); + + auto it = root.begin(); + const PTree* p_x = &it->second; // address of first child + ++it; + const PTree* p_y = &it->second; // address of second child + + // Different positions must give different addresses (different children). + BOOST_CHECK_NE(p_x, p_y); + BOOST_CHECK(it->first == "y"); + BOOST_CHECK(it->second.get_value() == "2"); +} + +BOOST_AUTO_TEST_CASE(iterator_structured_bindings_work_over_large_nested_tree) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + for (int group = 0; group < 5; ++group) { + PTree sub; + for (int item = 0; item < 20; ++item) { + sub.put("item" + std::to_string(item), std::string("val") + std::to_string(group * 20 + item)); + } + root.add_child("group" + std::to_string(group), sub); + } + + int groups_seen = 0; + int total_items = 0; + for (const auto& [gk, gv] : root) { + ++groups_seen; + BOOST_CHECK(gk.substr(0, 5) == "group"); + for (const auto& [ik, iv] : gv) { + ++total_items; + BOOST_CHECK(!iv.get_value().empty()); + } + } + + BOOST_CHECK_EQUAL(groups_seen, 5); + BOOST_CHECK_EQUAL(total_items, 100); +} + +BOOST_AUTO_TEST_CASE(iterator_post_increment_copy_holds_correct_view_of_old_position) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + root.add_child("first_key", PTree(std::string("first_val"))); + root.add_child("second_key", PTree(std::string("second_val"))); + + auto it = root.begin(); + auto prev = it++; // prev = copy before advance, it = second child + + BOOST_CHECK(prev->first == "first_key"); + BOOST_CHECK(prev->second.get_value() == "first_val"); + BOOST_CHECK(it->first == "second_key"); + BOOST_CHECK(it->second.get_value() == "second_val"); +} + +BOOST_AUTO_TEST_CASE(vsettings_put_int_get_int_round_trip_at_nested_path) { + ECF_NAME_THIS_TEST(); + + ecf::PTree pt; + pt.put("geom.x", 100); + pt.put("geom.y", 200); + BOOST_CHECK(pt.get("geom.x", 0) == 100); + BOOST_CHECK(pt.get("geom.y", 0) == 200); +} + +BOOST_AUTO_TEST_CASE(vsettings_booleans_stored_as_true_or_false_strings) { + ECF_NAME_THIS_TEST(); + + ecf::PTree pt; + pt.put("notification.aborted.enabled", std::string("true")); + pt.put("notification.submitted.enabled", std::string("false")); + + BOOST_CHECK(pt.get("notification.aborted.enabled", "false") == "true"); + BOOST_CHECK(pt.get("notification.submitted.enabled", "true") == "false"); +} + +BOOST_AUTO_TEST_CASE(vsettings_string_get_with_cstring_default) { + ECF_NAME_THIS_TEST(); + + ecf::PTree pt; + pt.put("theme", std::string("dark")); + BOOST_CHECK(pt.get("theme", "light") == "dark"); + BOOST_CHECK(pt.get("missing", "light") == "light"); +} + +BOOST_AUTO_TEST_CASE(vsettings_get_child_optional_iterate_array) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + std::vector expected = {"a.def", "b.def", "c.def"}; + PTree pt; + PTree arr; + for (const auto& s : expected) { + arr.push_back_array_element(PTree(s)); + } + pt.put_child("recentFiles", arr); + + auto opt = pt.get_child_optional("recentFiles"); + BOOST_REQUIRE(opt.has_value()); + + std::vector result; + for (const auto& [k, v] : *opt) { + result.push_back(v.get_value()); + } + BOOST_CHECK(result == expected); +} + +BOOST_AUTO_TEST_CASE(vsettings_get_child_optional_is_nullopt_for_absent_key) { + ECF_NAME_THIS_TEST(); + + ecf::PTree pt; + auto opt = pt.get_child_optional("nonexistent"); + BOOST_CHECK(!opt.has_value()); +} + +BOOST_AUTO_TEST_CASE(vsettings_array_of_subtrees_with_vector_vsettings_pattern) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + struct Session + { + std::string id, server; + int width; + }; + std::vector sessions = {{"0", "oper", 1280}, {"1", "rd", 800}, {"2", "backup", 640}}; + + PTree pt; + PTree arr; + for (const auto& s : sessions) { + PTree sub; + sub.put("id", s.id); + sub.put("server", s.server); + sub.put("width", s.width); + arr.push_back_array_element(sub); + } + pt.put_child("sessions", arr); + + // Retrieve and verify + auto opt = pt.get_child_optional("sessions"); + BOOST_REQUIRE(opt.has_value()); + + std::vector retrieved; + for (const auto& [k, v] : *opt) { + retrieved.push_back({v.get("id"), v.get("server"), v.get("width")}); + } + + BOOST_REQUIRE(retrieved.size() == 3); + BOOST_CHECK(retrieved[0].id == "0"); + BOOST_CHECK(retrieved[0].server == "oper"); + BOOST_CHECK(retrieved[0].width == 1280); + BOOST_CHECK(retrieved[1].id == "1"); + BOOST_CHECK(retrieved[1].server == "rd"); + BOOST_CHECK(retrieved[1].width == 800); + BOOST_CHECK(retrieved[2].id == "2"); + BOOST_CHECK(retrieved[2].server == "backup"); + BOOST_CHECK(retrieved[2].width == 640); +} + +BOOST_AUTO_TEST_CASE(vsettings_clear_empties_the_whole_tree) { + ECF_NAME_THIS_TEST(); + + ecf::PTree pt; + pt.put("a.b.c", std::string("deep")); + pt.put("x", 99); + BOOST_CHECK(!pt.empty()); + + pt.clear(); + BOOST_CHECK(pt.empty()); + BOOST_CHECK(!pt.contains("a")); + BOOST_CHECK(!pt.contains("x")); +} + +BOOST_AUTO_TEST_CASE(vconfig_leaf_vs_subtree_empty_distinguishes_them) { + ECF_NAME_THIS_TEST(); + + ecf::PTree root; + root.put("gui.default", std::string("10")); // leaf + root.put("gui.section.x", std::string("y")); // section has children + + // Leaf node (string) + auto opt_leaf = root.get_child_optional("gui.default"); + BOOST_REQUIRE(opt_leaf.has_value()); + BOOST_CHECK(opt_leaf->empty()); // leaf → empty = true (no children) + BOOST_CHECK(opt_leaf->is_leaf()); + + // Sub-section (object with children) + auto opt_sec = root.get_child_optional("gui.section"); + BOOST_REQUIRE(opt_sec.has_value()); + BOOST_CHECK(!opt_sec->empty()); // has children → empty = false + BOOST_CHECK(!opt_sec->is_leaf()); +} + +BOOST_AUTO_TEST_CASE(vconfig_loadimportedsettings_pattern) { + ECF_NAME_THIS_TEST(); + + ecf::PTree imported; + imported.put("server.host", std::string("prod.ecflow.int")); + imported.put("server.timeout", std::string("60")); + + if (auto opt = imported.get_child_optional("server")) { + BOOST_CHECK(opt->get("host", "localhost") == "prod.ecflow.int"); + BOOST_CHECK(opt->get("timeout", "30") == "60"); + BOOST_CHECK(opt->get("missing", "none") == "none"); + } + else { + BOOST_FAIL("expected server subtree to exist"); + } +} + +BOOST_AUTO_TEST_CASE(vconfig_savesettings_splice_top_level_children_via_add_child) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree server_settings; + server_settings.put("host", std::string("oper")); + server_settings.put("port", 3141); + server_settings.put("timeout", 30); + + // "global" config tree starts with some data + PTree global; + global.put("version", std::string("7.0.0")); + + // Splice + for (const auto& [k, v] : server_settings) { + global.add_child(k, v); + } + + BOOST_CHECK(global.get("version") == "7.0.0"); + BOOST_CHECK(global.get("host") == "oper"); + BOOST_CHECK(global.get("port") == 3141); + BOOST_CHECK(global.get("timeout") == 30); +} + +BOOST_AUTO_TEST_CASE(vconfig_readrcfile_pattern_build_tree_from_key_value_pairs_suite_list_array) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree pt; + pt.put("server.name", std::string("oper_server")); + pt.put("server.host", std::string("ecflow-oper.ecmwf.int")); + pt.put("server.port", std::string("3141")); // rc files use strings + + std::vector suite_names = {"oper", "rd", "wave"}; + PTree suites; + for (const auto& s : suite_names) { + suites.push_back_array_element(PTree(s)); + } + pt.put_child("suites", suites); + + BOOST_CHECK(pt.get("server.name") == "oper_server"); + BOOST_CHECK(pt.get("server.host") == "ecflow-oper.ecmwf.int"); + BOOST_CHECK(pt.get("server.port", 0) == 3141); + + auto opt = pt.get_child_optional("suites"); + BOOST_REQUIRE(opt.has_value()); + std::vector result; + for (const auto& [k, v] : *opt) { + result.push_back(v.get_value()); + } + BOOST_CHECK(result == suite_names); +} + +BOOST_AUTO_TEST_CASE(menuhandler_get_with_string_literal_default_on_each_field) { + ECF_NAME_THIS_TEST(); + + ecf::PTree item; + item.put("command", std::string("execute")); + item.put("handler", std::string("NodeHandler")); + item.put("icon", std::string("run.png")); + + BOOST_CHECK(item.get("command", "NoCommand") == "execute"); + BOOST_CHECK(item.get("handler", "") == "NodeHandler"); + BOOST_CHECK(item.get("icon", "") == "run.png"); + BOOST_CHECK(item.get("tooltip", "") == ""); // uses default + BOOST_CHECK(item.get("enabled", "false") == "false"); // uses default +} + +BOOST_AUTO_TEST_CASE(menuhandler_top_level_plus_nested_iteration_order_preserved) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + PTree root; + + // menus array + PTree menus; + for (const auto& name : {"Node", "Server", "NodeContext"}) { + PTree m; + m.put("name", std::string(name)); + menus.push_back_array_element(m); + } + root.put_child("menus", menus); + + // menu_items array + PTree items; + for (const auto& cmd : {"execute", "suspend", "requeue"}) { + PTree item; + item.put("command", std::string(cmd)); + item.put("handler", std::string("NodeHandler")); + items.push_back_array_element(item); + } + root.put_child("menu_items", items); + + // Verify order + std::vector menu_names, item_cmds; + auto menus_opt = root.get_child_optional("menus"); + auto items_opt = root.get_child_optional("menu_items"); + BOOST_REQUIRE(menus_opt.has_value()); + BOOST_REQUIRE(items_opt.has_value()); + for (const auto& [k, v] : *menus_opt) { + menu_names.push_back(v.get("name", "")); + } + for (const auto& [k, v] : *items_opt) { + item_cmds.push_back(v.get("command", "")); + } + + BOOST_CHECK(menu_names == std::vector({"Node", "Server", "NodeContext"})); + BOOST_CHECK(item_cmds == std::vector({"execute", "suspend", "requeue"})); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_simple_json) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(test_data_file("simple.json"), t)); + + BOOST_CHECK(t.get("name") == "Alice"); + BOOST_CHECK(t.get("age") == 30); + BOOST_CHECK(t.get("address.city") == "London"); + BOOST_CHECK(t.get("address.zip") == "EC1A 1BB"); + BOOST_CHECK(t.contains("empty_obj")); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_settings_json_with_string_encoded_scalars) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(test_data_file("settings.json"), t)); + + // Top-level scalars stored as strings (boost convention) + BOOST_CHECK(t.get("tabCount") == "3"); + BOOST_CHECK(t.get("currentTabId") == "0"); + BOOST_CHECK(t.get("theme") == "dark"); + + // Coerce string-encoded int + BOOST_CHECK(t.get("tabCount", 0) == 3); + + // Nested path + BOOST_CHECK(t.get("server.notification.aborted.enabled") == "true"); + BOOST_CHECK(t.get("server.notification.aborted.maxItems") == "100"); + BOOST_CHECK(t.get("geom.x") == "100"); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_settings_json_with_favourites_array_in_order) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + read_json(test_data_file("settings.json"), t); + + auto opt = t.get_child_optional("favourites"); + BOOST_REQUIRE(opt.has_value()); + + std::vector hosts; + for (const auto& [k, v] : *opt) { + BOOST_CHECK(k.empty()); // anonymous key == array element + hosts.push_back(v.get_value()); + } + BOOST_REQUIRE(hosts.size() == 3); + BOOST_CHECK(hosts[0] == "host1:3141"); + BOOST_CHECK(hosts[1] == "host2:3141"); + BOOST_CHECK(hosts[2] == "host3:3141"); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_settings_json_tabs_array_of_objects) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + read_json(test_data_file("settings.json"), t); + + auto opt = t.get_child_optional("tabs"); + BOOST_REQUIRE(opt.has_value()); + + std::vector ids, names; + for (const auto& [k, v] : *opt) { + BOOST_CHECK(k.empty()); + ids.push_back(v.get("id")); + names.push_back(v.get("name")); + } + BOOST_REQUIRE(ids.size() == 3); + BOOST_CHECK(ids[0] == "0"); + BOOST_CHECK(names[0] == "Operations"); + BOOST_CHECK(ids[1] == "1"); + BOOST_CHECK(names[1] == "Research"); + BOOST_CHECK(ids[2] == "2"); + BOOST_CHECK(names[2] == "Backup"); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_menu_json_following_menuhandler_infopanelhandler_pattern) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(test_data_file("menu.json"), t)); + + // Top-level keys: menus and menu_items + auto menus_opt = t.get_child_optional("menus"); + BOOST_REQUIRE(menus_opt.has_value()); + int menu_count = 0; + for ([[maybe_unused]] const auto& m : *menus_opt) { + ++menu_count; + } + BOOST_CHECK(menu_count == 3); + + auto items_opt = t.get_child_optional("menu_items"); + BOOST_REQUIRE(items_opt.has_value()); + + // First item + auto it = items_opt->begin(); + BOOST_REQUIRE(it != items_opt->end()); + BOOST_CHECK(it->second.get("command", std::string("NoCommand")) == "execute"); + BOOST_CHECK(it->second.get("handler", std::string("")) == "NodeHandler"); + BOOST_CHECK(it->second.get("enabled", std::string("false")) == "true"); + BOOST_CHECK(it->second.get("missing_key", std::string("def")) == "def"); // default fallback +} + +BOOST_AUTO_TEST_CASE(read_json_parses_palette_json_with_2_level_iteration) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(test_data_file("palette.json"), t)); + + std::vector groups; + for (const auto& [group, roles] : t) { + groups.push_back(group); + for (const auto& [role, colour] : roles) { + BOOST_CHECK(!colour.get_value().empty()); + } + } + BOOST_REQUIRE(groups.size() == 3); + BOOST_CHECK(groups[0] == "active"); + BOOST_CHECK(groups[1] == "inactive"); + BOOST_CHECK(groups[2] == "disabled"); +} + +BOOST_AUTO_TEST_CASE(read_json_parses_highlighter_json_with_find_and_iterate_rules) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(test_data_file("highlighter.json"), t)); + + // Highlighter.cpp pattern: pt.find(id) / pt.not_found() + auto itTop = t.find("ecfscript"); + BOOST_REQUIRE(itTop != t.end()); + + std::vector patterns; + for (const auto& [rule_key, rule_node] : itTop->second) { + auto itPat = rule_node.find("pattern"); + if (itPat != rule_node.end()) { + patterns.push_back(itPat->second.get_value()); + } + } + BOOST_CHECK(patterns.size() == 2); + + // Unknown group exists but is empty + auto itUnknown = t.find("unknown_group"); + BOOST_REQUIRE(itUnknown != t.end()); + int count = 0; + for ([[maybe_unused]] const auto& item : itUnknown->second) { + ++count; + } + BOOST_CHECK(count == 0); +} + +BOOST_AUTO_TEST_CASE(write_json_produces_a_readable_file_with_read_json_round_trip) { + ECF_NAME_THIS_TEST(); + + using ecf::PTree; + + // Build a tree in memory + PTree original; + original.put("version", std::string("2.0")); + original.put("server.host", std::string("oper.ecflow")); + original.put("server.port", 3141); + + PTree favs; + favs.push_back_array_element(PTree("host1:3141")); + favs.push_back_array_element(PTree("host2:3141")); + original.put_child("favourites", favs); + + // Write to a temp file + auto tmp = fs::temp_directory_path() / "pprop_roundtrip_test.json"; + BOOST_REQUIRE_NO_THROW(write_json(tmp.string(), original)); + BOOST_CHECK(fs::exists(tmp)); + + // Re-read + PTree restored; + BOOST_REQUIRE_NO_THROW(read_json(tmp.string(), restored)); + + BOOST_CHECK(restored.get("version") == "2.0"); + BOOST_CHECK(restored.get("server.host") == "oper.ecflow"); + BOOST_CHECK(restored.get("server.port") == 3141); + + auto opt = restored.get_child_optional("favourites"); + BOOST_REQUIRE(opt.has_value()); + std::vector hosts; + for (const auto& [k, v] : *opt) { + hosts.push_back(v.get_value()); + } + BOOST_CHECK(hosts == std::vector({"host1:3141", "host2:3141"})); + + fs::remove(tmp); +} + +BOOST_AUTO_TEST_CASE(write_json_produces_pretty_printed_output) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("a", std::string("1")); + t.put("b", std::string("2")); + + auto tmp = fs::temp_directory_path() / "pprop_pretty_test.json"; + write_json(tmp.string(), t); + + std::ifstream fs_in(tmp); + BOOST_REQUIRE(fs_in.is_open()); + std::string content((std::istreambuf_iterator(fs_in)), std::istreambuf_iterator()); + + // Should contain newlines (pretty-printed, not compact) + BOOST_CHECK(content.find('\n') != std::string::npos); + + fs::remove(tmp); +} + +BOOST_AUTO_TEST_CASE(full_settings_json_round_trip_preserves_array_order) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + read_json(test_data_file("settings.json"), t); + + auto tmp = fs::temp_directory_path() / "pprop_settings_roundtrip.json"; + write_json(tmp.string(), t); + + ecf::PTree t2; + read_json(tmp.string(), t2); + + // Array insertion order must be preserved + auto opt1 = t.get_child_optional("favourites"); + auto opt2 = t2.get_child_optional("favourites"); + BOOST_REQUIRE(opt1.has_value()); + BOOST_REQUIRE(opt2.has_value()); + + std::vector h1, h2; + for (const auto& [k, v] : *opt1) { + h1.push_back(v.get_value()); + } + for (const auto& [k, v] : *opt2) { + h2.push_back(v.get_value()); + } + BOOST_CHECK(h1 == h2); + + // Object key order must be preserved + std::vector k1, k2; + for (const auto& [k, v] : t) { + k1.push_back(k); + } + for (const auto& [k, v] : t2) { + k2.push_back(k); + } + BOOST_CHECK(k1 == k2); + + fs::remove(tmp); +} + +BOOST_AUTO_TEST_CASE(read_json_throws_JsonParseError_for_nonexistent_file) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + BOOST_CHECK_THROW(read_json("/no/such/file.json", t), ecf::PTreeParseError); +} + +BOOST_AUTO_TEST_CASE(read_json_throws_JsonParseError_for_malformed_json) { + ECF_NAME_THIS_TEST(); + + auto tmp = fs::temp_directory_path() / "pprop_bad.json"; + { + std::ofstream out(tmp); + out << "{ this is not : valid json ,,, }"; + } + ecf::PTree t; + BOOST_CHECK_THROW(read_json(tmp.string(), t), ecf::PTreeParseError); + fs::remove(tmp); +} + +BOOST_AUTO_TEST_CASE(JsonParseError_is_a_std_runtime_error) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + bool caught = false; + try { + read_json("/no/such/file.json", t); + } + catch (const std::runtime_error&) { + caught = true; + } + BOOST_CHECK(caught); +} + +BOOST_AUTO_TEST_CASE(write_json_throws_JsonParseError_for_unwritable_path) { + ECF_NAME_THIS_TEST(); + + ecf::PTree t; + t.put("x", std::string("1")); + BOOST_CHECK_THROW(write_json("/no/such/directory/file.json", t), ecf::PTreeParseError); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/libs/core/test/data/PTree/highlighter.json b/libs/core/test/data/PTree/highlighter.json new file mode 100644 index 000000000..f4fccb87e --- /dev/null +++ b/libs/core/test/data/PTree/highlighter.json @@ -0,0 +1,22 @@ +{ + "ecfscript": [ + { + "pattern": "^%[A-Z_]+%", + "color": "#0000cc", + "bold": "true" + }, + { + "pattern": "#.*$", + "color": "#808080", + "bold": "false" + } + ], + "triggers": [ + { + "pattern": "\\b(complete|aborted|submitted|queued|active)\\b", + "color": "#cc0000", + "bold": "false" + } + ], + "unknown_group": [] +} diff --git a/libs/core/test/data/PTree/menu.json b/libs/core/test/data/PTree/menu.json new file mode 100644 index 000000000..7f8c234ff --- /dev/null +++ b/libs/core/test/data/PTree/menu.json @@ -0,0 +1,48 @@ +{ + "menus": [ + { + "name": "Node", + "parent": "" + }, + { + "name": "Server", + "parent": "" + }, + { + "name": "NodeContext", + "parent": "Node" + } + ], + "menu_items": [ + { + "name": "Execute", + "command": "execute", + "handler": "NodeHandler", + "icon": "run.png", + "tooltip": "Submit the selected node", + "enabled": "true", + "hidden": "false", + "separator": "false" + }, + { + "name": "Suspend", + "command": "suspend", + "handler": "NodeHandler", + "icon": "pause.png", + "tooltip": "Suspend the selected node", + "enabled": "true", + "hidden": "false", + "separator": "false" + }, + { + "name": "---", + "command": "NoCommand", + "handler": "", + "icon": "", + "tooltip": "", + "enabled": "false", + "hidden": "false", + "separator": "true" + } + ] +} diff --git a/libs/core/test/data/PTree/palette.json b/libs/core/test/data/PTree/palette.json new file mode 100644 index 000000000..872ead0ef --- /dev/null +++ b/libs/core/test/data/PTree/palette.json @@ -0,0 +1,23 @@ +{ + "active": { + "Window": "#f0f0f0", + "WindowText": "#000000", + "Base": "#ffffff", + "Button": "#e0e0e0", + "Highlight": "#0078d4" + }, + "inactive": { + "Window": "#d8d8d8", + "WindowText": "#555555", + "Base": "#f8f8f8", + "Button": "#cccccc", + "Highlight": "#4a9fcf" + }, + "disabled": { + "Window": "#c0c0c0", + "WindowText": "#888888", + "Base": "#eeeeee", + "Button": "#b8b8b8", + "Highlight": "#999999" + } +} diff --git a/libs/core/test/data/PTree/settings.json b/libs/core/test/data/PTree/settings.json new file mode 100644 index 000000000..be2ca8b3d --- /dev/null +++ b/libs/core/test/data/PTree/settings.json @@ -0,0 +1,50 @@ +{ + "geom": { + "x": "100", + "y": "200", + "w": "1280", + "h": "800" + }, + "tabCount": "3", + "currentTabId": "0", + "theme": "dark", + "favourites": [ + "host1:3141", + "host2:3141", + "host3:3141" + ], + "recentFiles": [ + "/tmp/a.def", + "/tmp/b.def" + ], + "tabs": [ + { + "id": "0", + "name": "Operations", + "server": "oper" + }, + { + "id": "1", + "name": "Research", + "server": "rd" + }, + { + "id": "2", + "name": "Backup", + "server": "backup" + } + ], + "server": { + "notification": { + "aborted": { + "enabled": "true", + "maxItems": "100" + }, + "submitted": { + "enabled": "false", + "maxItems": "50" + } + }, + "timeout": "30" + } +} diff --git a/libs/core/test/data/PTree/simple.json b/libs/core/test/data/PTree/simple.json new file mode 100644 index 000000000..a8fde9540 --- /dev/null +++ b/libs/core/test/data/PTree/simple.json @@ -0,0 +1,12 @@ +{ + "name": "Alice", + "age": 30, + "active": true, + "score": 9.5, + "address": { + "city": "London", + "zip": "EC1A 1BB" + }, + "empty_obj": {}, + "null_val": null +} From 44f11da20e210dbddc4705e1d66481a52aee75a5 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:00:47 +0100 Subject: [PATCH 3/8] Update known limitations for PTree --- libs/core/src/ecflow/core/PTree.hpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/libs/core/src/ecflow/core/PTree.hpp b/libs/core/src/ecflow/core/PTree.hpp index 5ff1e410c..aa4d7850f 100644 --- a/libs/core/src/ecflow/core/PTree.hpp +++ b/libs/core/src/ecflow/core/PTree.hpp @@ -69,11 +69,7 @@ void read_json(const std::string& filename, PTree& out); /// @brief Serialize a property tree to JSON and write it to a file. /// /// @attention Writing a PTree with repeated keys WILL TRUNCATE the output -/// (JSON objects cannot have duplicate keys). -/// -/// This is acceptable because: -/// * Files with repeated keys (e.g. ecflowview_gui.json) are never written back by ecFlow. -/// * User settings written by write_json always/only have unique keys. +/// (see Known Limitation of PTree class). /// /// @param filename the name of the output file /// @param in a property tree to serialize @@ -99,6 +95,19 @@ void write_json(const std::string& filename, const PTree& in, int indent = 4); /// /// 3) Implemented as a replacement for boost::property_tree::ptree. /// +/// * Known Limitations +/// +/// 1) Writing a PTree with repeated keys WILL TRUNCATE the output +/// (JSON objects cannot have duplicate keys). +/// +/// This is acceptable because: +/// * Files with repeated keys (e.g. ecflowview_gui.json) are never written back by ecFlow. +/// * User settings written by write_json always/only have unique keys. +/// +/// 2) Large number values are silently truncated to fit into an int. +/// +/// This is acceptable because ecFlow settings files are not expected to contain very large numbers. +/// /// * Node structure /// /// Each PTree node is a named value: From de9bebc943bd159c5f3a0be0c6c550ddf51aed47 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:08:46 +0100 Subject: [PATCH 4/8] Remove noexcept --- libs/core/src/ecflow/core/PTree.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/core/src/ecflow/core/PTree.hpp b/libs/core/src/ecflow/core/PTree.hpp index aa4d7850f..cb3ef8e65 100644 --- a/libs/core/src/ecflow/core/PTree.hpp +++ b/libs/core/src/ecflow/core/PTree.hpp @@ -252,7 +252,7 @@ class PTree { /// or `default_value` if the path is absent or if type conversion fails /// template - T get(std::string_view path, T default_value) const noexcept; + T get(std::string_view path, T default_value) const; /// Convenience overloads for const char* / string literal defaults. std::string get(std::string_view path, const char* dv) const; @@ -548,7 +548,7 @@ inline bool PTree::get_value() const { } template -T PTree::get(std::string_view path, T default_value) const noexcept { +T PTree::get(std::string_view path, T default_value) const { const PTree* node = navigate(path); if (!node) { return default_value; From 3cf5117b2299d9b90268598c688a418882273df0 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:21:04 +0100 Subject: [PATCH 5/8] Avoid corruption of arrays through navigation --- libs/core/src/ecflow/core/PTree.cpp | 10 +++ libs/core/src/ecflow/core/PTree.hpp | 1 + libs/core/test/TestPTree.cpp | 130 ++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/libs/core/src/ecflow/core/PTree.cpp b/libs/core/src/ecflow/core/PTree.cpp index 0d21572ec..3322caa3b 100644 --- a/libs/core/src/ecflow/core/PTree.cpp +++ b/libs/core/src/ecflow/core/PTree.cpp @@ -70,6 +70,16 @@ PTree& PTree::navigate_or_create(std::string_view path) { } PTree& PTree::get_or_create_child(const std::string& key) { + if (kind_ == Kind::Children && arr_) { + // + // _Navigating_ into an array with a named key is always a programming error. + // + // This check prevents to silently append a named child to the array, + // and thus mixing anonymous and named elements in the same children + // vector and corrupting the node. + // + throw PTreeInvalidStateError("PTree: cannot navigate into an array node with key '" + key + "'"); + } if (kind_ != Kind::Children) { kind_ = Kind::Children; arr_ = false; diff --git a/libs/core/src/ecflow/core/PTree.hpp b/libs/core/src/ecflow/core/PTree.hpp index cb3ef8e65..6a323d508 100644 --- a/libs/core/src/ecflow/core/PTree.hpp +++ b/libs/core/src/ecflow/core/PTree.hpp @@ -404,6 +404,7 @@ class PTree { /// /// @param key the key associated to the child (not a dotted path) /// @return the node associated to the key, or a new null node if the key was not found among direct children + /// @throws PTreeInvalidStateError if this node is an array (mixing named and anonymous children is not permitted). /// PTree& get_or_create_child(const std::string& key); diff --git a/libs/core/test/TestPTree.cpp b/libs/core/test/TestPTree.cpp index 594901fb2..df5fe85bc 100644 --- a/libs/core/test/TestPTree.cpp +++ b/libs/core/test/TestPTree.cpp @@ -1531,6 +1531,136 @@ BOOST_AUTO_TEST_CASE(write_json_throws_JsonParseError_for_unwritable_path) { BOOST_CHECK_THROW(write_json("/no/such/directory/file.json", t), ecf::PTreeParseError); } +BOOST_AUTO_TEST_CASE(get_or_create_child_on_array_via_put_string_throws) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("alpha"))); + arr.push_back_array_element(ecf::PTree(std::string("beta"))); + + BOOST_CHECK_THROW(arr.put("key", std::string("x")), ecf::PTreeInvalidStateError); +} + +BOOST_AUTO_TEST_CASE(get_or_create_child_on_array_via_put_int_throws) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("alpha"))); + + BOOST_CHECK_THROW(arr.put("count", 42), ecf::PTreeInvalidStateError); +} + +BOOST_AUTO_TEST_CASE(get_or_create_child_on_array_via_put_child_throws) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("alpha"))); + + ecf::PTree child; + child.put("x", std::string("1")); + BOOST_CHECK_THROW(arr.put_child("sub", std::move(child)), ecf::PTreeInvalidStateError); +} + +BOOST_AUTO_TEST_CASE(get_or_create_child_error_message_contains_key_name) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("v"))); + + try { + arr.put("offending_key", std::string("x")); + BOOST_FAIL("expected PTreeInvalidStateError"); + } + catch (const ecf::PTreeInvalidStateError& e) { + BOOST_CHECK(std::string(e.what()).find("offending_key") != std::string::npos); + } +} + +BOOST_AUTO_TEST_CASE(get_or_create_child_array_is_not_corrupted_after_throw) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("first"))); + arr.push_back_array_element(ecf::PTree(std::string("second"))); + + try { + arr.put("key", std::string("x")); + } + catch (const ecf::PTreeInvalidStateError&) { /* expected */ } + + // Must still be an array + BOOST_REQUIRE(arr.is_array()); + + // Must still have exactly 2 anonymous elements with the original values + std::vector elems; + for (const auto& [k, v] : arr) { + BOOST_CHECK(k.empty()); // array elements have empty names + elems.push_back(v.get_value()); + } + BOOST_REQUIRE_EQUAL(elems.size(), 2u); + BOOST_CHECK_EQUAL(elems[0], "first"); + BOOST_CHECK_EQUAL(elems[1], "second"); +} + +BOOST_AUTO_TEST_CASE(get_or_create_child_intermediate_array_in_dotted_path_throws) { + ECF_NAME_THIS_TEST(); + + // Build: root → tabs (array of two strings) + ecf::PTree tabs; + tabs.push_back_array_element(ecf::PTree(std::string("tab1"))); + tabs.push_back_array_element(ecf::PTree(std::string("tab2"))); + ecf::PTree root; + root.put_child("tabs", std::move(tabs)); + + // Navigating INTO the array via a dotted path must throw + BOOST_CHECK_THROW(root.put("tabs.count", 2), ecf::PTreeInvalidStateError); + BOOST_CHECK_THROW(root.put("tabs.name", std::string("list")), ecf::PTreeInvalidStateError); + + // The tabs array must remain intact + auto t = root.get_child_optional("tabs"); + BOOST_REQUIRE(t.has_value()); + BOOST_REQUIRE(t->is_array()); + int count = 0; + for ([[maybe_unused]] const auto& child : *t) { + ++count; + } + BOOST_CHECK_EQUAL(count, 2); +} + +BOOST_AUTO_TEST_CASE(put_child_replacing_an_array_node_does_not_throw) { + ECF_NAME_THIS_TEST(); + + ecf::PTree old_arr; + old_arr.push_back_array_element(ecf::PTree(std::string("old"))); + ecf::PTree root; + root.put_child("list", std::move(old_arr)); + BOOST_REQUIRE(root.get_child_optional("list")->is_array()); + + // Replace the array with a plain object subtree + ecf::PTree replacement; + replacement.put("name", std::string("replaced")); + BOOST_REQUIRE_NO_THROW(root.put_child("list", std::move(replacement))); + + BOOST_CHECK_EQUAL(root.get("list.name", ""), "replaced"); + BOOST_CHECK(!root.get_child_optional("list")->is_array()); +} + +BOOST_AUTO_TEST_CASE(PTreeInvalidStateError_from_array_guard_is_catchable_as_std_runtime_error) { + ECF_NAME_THIS_TEST(); + + ecf::PTree arr; + arr.push_back_array_element(ecf::PTree(std::string("v"))); + + bool caught = false; + try { + arr.put("key", std::string("x")); + } + catch (const std::runtime_error&) { + caught = true; + } + BOOST_CHECK(caught); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From 5d9c0e103a95c1f07d822413f8b9e19ad6eff421 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:34:45 +0100 Subject: [PATCH 6/8] Fix typos --- Viewer/ecflowUI/src/InfoPanelHandler.cpp | 2 +- Viewer/ecflowUI/src/MenuHandler.cpp | 2 +- Viewer/ecflowUI/src/VConfig.cpp | 4 ++-- Viewer/libViewer/src/Palette.cpp | 2 +- libs/core/src/ecflow/core/PTree.cpp | 2 +- libs/core/src/ecflow/core/PTree.hpp | 10 +++++----- libs/core/test/TestPTree.cpp | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Viewer/ecflowUI/src/InfoPanelHandler.cpp b/Viewer/ecflowUI/src/InfoPanelHandler.cpp index 08adf33e6..703cf888c 100644 --- a/Viewer/ecflowUI/src/InfoPanelHandler.cpp +++ b/Viewer/ecflowUI/src/InfoPanelHandler.cpp @@ -35,7 +35,7 @@ InfoPanelHandler* InfoPanelHandler::instance() { } void InfoPanelHandler::init(const std::string& configFile) { - // parse the response using the boost JSON property tree parser + // Parse JSON file using ecf::PTree (SAX-based, supports repeated keys) ecf::PTree pt; diff --git a/Viewer/ecflowUI/src/MenuHandler.cpp b/Viewer/ecflowUI/src/MenuHandler.cpp index 032d58048..c92c6f35d 100644 --- a/Viewer/ecflowUI/src/MenuHandler.cpp +++ b/Viewer/ecflowUI/src/MenuHandler.cpp @@ -58,7 +58,7 @@ MenuHandler::MenuHandler() { // --------------------------------------------------------- bool MenuHandler::readMenuConfigFile(const std::string& configFile) { - // parse the response using the boost JSON property tree parser + // Parse JSON file using ecf::PTree (SAX-based, supports repeated keys) ecf::PTree pt; diff --git a/Viewer/ecflowUI/src/VConfig.cpp b/Viewer/ecflowUI/src/VConfig.cpp index 512e2b699..40492d4b3 100644 --- a/Viewer/ecflowUI/src/VConfig.cpp +++ b/Viewer/ecflowUI/src/VConfig.cpp @@ -90,7 +90,7 @@ void VConfig::init(const std::string& parDirPath) { } void VConfig::loadInit(const std::string& parFile) { - // Parse param file using the boost JSON property tree parser + // Parse JSON file using ecf::PTree (SAX-based, supports repeated keys) ecf::PTree pt; try { @@ -300,7 +300,7 @@ void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool std::vector linkVec; guiProp->collectLinks(linkVec); - // Parse file using the boost JSON property tree parser + // Parse JSON file using ecf::PTree (SAX-based, supports repeated keys) ecf::PTree pt; try { diff --git a/Viewer/libViewer/src/Palette.cpp b/Viewer/libViewer/src/Palette.cpp index 24db1ee75..5cd85910a 100644 --- a/Viewer/libViewer/src/Palette.cpp +++ b/Viewer/libViewer/src/Palette.cpp @@ -54,7 +54,7 @@ void Palette::load(const std::string& parFile) { paletteId["linkvisited"] = QPalette::LinkVisited; } - // Parse param file using the boost JSON property tree parser + // Parse JSON file using ecf::PTree (SAX-based, supports repeated keys) ecf::PTree pt; try { diff --git a/libs/core/src/ecflow/core/PTree.cpp b/libs/core/src/ecflow/core/PTree.cpp index 3322caa3b..51c76e450 100644 --- a/libs/core/src/ecflow/core/PTree.cpp +++ b/libs/core/src/ecflow/core/PTree.cpp @@ -139,7 +139,7 @@ PTree::const_iterator_t PTree::find(std::string_view key) const { } static const PTree::children_t& sentinel_vec() { - // A atable empty vector is used as a sentinel so that begin() == end() + // A stable empty vector is used as a sentinel so that begin() == end() // for null/leaf nodes without undefined behaviour from cross-container comparisons. static const PTree::children_t sv; return sv; diff --git a/libs/core/src/ecflow/core/PTree.hpp b/libs/core/src/ecflow/core/PTree.hpp index 6a323d508..22ae1f7d5 100644 --- a/libs/core/src/ecflow/core/PTree.hpp +++ b/libs/core/src/ecflow/core/PTree.hpp @@ -318,8 +318,8 @@ class PTree { /// @brief Set a scalar integer at a dotted path, creating intermediate objects as needed. /// Important: Last-wins if the final key already exists!!! /// - /// @param path the dotted integer to set (e.g. "server.notification.enabled") - /// @param value the scalar string to set + /// @param path the dotted path to set (e.g. "server.notification.enabled") + /// @param value the scalar integer to set /// void put(std::string_view path, int value); @@ -329,7 +329,7 @@ class PTree { /// If path is empty, appends as an anonymous array element /// — this is to handle the existing pattern `tree.put_child("", value)` used in readRcFile. /// - /// @param path the dotted integer to set (e.g. "server.notification.enabled") + /// @param path the dotted path to set (e.g. "server.notification.enabled") /// @param child the subtree /// void put_child(std::string_view path, PTree child); @@ -370,12 +370,12 @@ class PTree { private: name_t name_{}; ///< This node's own name (key in the parent's children list). - Kind kind_{Kind::Null}; ///< when kind_==Children: true → array ("" names), false → object + Kind kind_{Kind::Null}; ///< discriminant: Null, String, Int, or Children // Object std::string str_{}; int int_{0}; - bool arr_{false}; + bool arr_{false}; ///< when kind_==Children: true → array ("" names), false → object // Array children_t children_{}; ///< Each child carries its own name_ member. diff --git a/libs/core/test/TestPTree.cpp b/libs/core/test/TestPTree.cpp index df5fe85bc..68716e601 100644 --- a/libs/core/test/TestPTree.cpp +++ b/libs/core/test/TestPTree.cpp @@ -20,7 +20,7 @@ namespace { -static std::string test_data_file(const std::string& name) { +std::string test_data_file(const std::string& name) { std::string path = ecf::File::test_data("libs/core/test/data/PTree", "libs/core"); return path + "/" + name; } From 3d9e40093dab972594463c67c0cce1e52969074c Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:44:12 +0100 Subject: [PATCH 7/8] Add a few more tests --- libs/core/test/TestPTree.cpp | 60 +++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/libs/core/test/TestPTree.cpp b/libs/core/test/TestPTree.cpp index 68716e601..d13cfbd24 100644 --- a/libs/core/test/TestPTree.cpp +++ b/libs/core/test/TestPTree.cpp @@ -94,7 +94,7 @@ BOOST_AUTO_TEST_CASE(ctor_copy_constructor_does_not_share_content) { BOOST_CHECK(b.get("x") == "99"); } -BOOST_AUTO_TEST_CASE(c_tor_move_constructor_transfers_content) { +BOOST_AUTO_TEST_CASE(ctor_move_constructor_transfers_content) { ECF_NAME_THIS_TEST(); using ecf::PTree; @@ -1661,6 +1661,64 @@ BOOST_AUTO_TEST_CASE(PTreeInvalidStateError_from_array_guard_is_catchable_as_std BOOST_CHECK(caught); } +BOOST_AUTO_TEST_CASE(get_value_bool_recognises_true_and_1_as_true) { + ECF_NAME_THIS_TEST(); + + BOOST_CHECK(ecf::PTree(std::string("true")).get_value() == true); + BOOST_CHECK(ecf::PTree(std::string("1")).get_value() == true); +} + +BOOST_AUTO_TEST_CASE(get_value_bool_recognises_false_and_0_as_false) { + ECF_NAME_THIS_TEST(); + + BOOST_CHECK(ecf::PTree(std::string("false")).get_value() == false); + BOOST_CHECK(ecf::PTree(std::string("0")).get_value() == false); +} + +BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_unrecognised_string) { + ECF_NAME_THIS_TEST(); + + BOOST_CHECK_THROW(ecf::PTree(std::string("yes")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("no")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("True")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("")).get_value(), ecf::PTreeValueError); +} + +BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_int_node) { + ECF_NAME_THIS_TEST(); + + BOOST_CHECK_THROW(ecf::PTree(1).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(0).get_value(), ecf::PTreeValueError); +} + +BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_null_node) { + ECF_NAME_THIS_TEST(); + + BOOST_CHECK_THROW(ecf::PTree{}.get_value(), ecf::PTreeValueError); +} + +BOOST_AUTO_TEST_CASE(read_json_number_large_unsigned_branch_is_exercised_without_crash) { + ECF_NAME_THIS_TEST(); + + auto tmp = fs::temp_directory_path() / "ptree_n6_unsigned.json"; + { + std::ofstream out(tmp); + // UINT64_MAX = 18446744073709551615 (> INT64_MAX, triggers number_unsigned) + // INT64_MAX+1 = 9223372036854775808 (also triggers number_unsigned) + out << R"({ "uint64max": 18446744073709551615, "int64_plus1": 9223372036854775808 })"; + } + + ecf::PTree t; + BOOST_REQUIRE_NO_THROW(read_json(tmp.string(), t)); + + // Both keys must be present + // These values are valid JSON numbers, but too big for int64_t, so will return invalid/different values + BOOST_CHECK(t.contains("uint64max")); + BOOST_CHECK(t.contains("int64_plus1")); + + fs::remove(tmp); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() From 155cb7f6116e6f78597a489e7759889a806afd92 Mon Sep 17 00:00:00 2001 From: Marcos Bento Date: Thu, 21 May 2026 21:57:14 +0100 Subject: [PATCH 8/8] Correct source code formatting --- Viewer/ecflowUI/src/MenuHandler.cpp | 2 +- libs/core/test/TestPTree.cpp | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Viewer/ecflowUI/src/MenuHandler.cpp b/Viewer/ecflowUI/src/MenuHandler.cpp index c92c6f35d..85a2f6660 100644 --- a/Viewer/ecflowUI/src/MenuHandler.cpp +++ b/Viewer/ecflowUI/src/MenuHandler.cpp @@ -37,8 +37,8 @@ #include "VNode.hpp" #include "VProperty.hpp" #include "ViewerUtil.hpp" -#include "ecflow/core/Str.hpp" #include "ecflow/core/PTree.hpp" +#include "ecflow/core/Str.hpp" int MenuItem::idCnt_ = 0; diff --git a/libs/core/test/TestPTree.cpp b/libs/core/test/TestPTree.cpp index d13cfbd24..cb0b94b9b 100644 --- a/libs/core/test/TestPTree.cpp +++ b/libs/core/test/TestPTree.cpp @@ -1586,7 +1586,8 @@ BOOST_AUTO_TEST_CASE(get_or_create_child_array_is_not_corrupted_after_throw) { try { arr.put("key", std::string("x")); } - catch (const ecf::PTreeInvalidStateError&) { /* expected */ } + catch (const ecf::PTreeInvalidStateError&) { /* expected */ + } // Must still be an array BOOST_REQUIRE(arr.is_array()); @@ -1665,30 +1666,30 @@ BOOST_AUTO_TEST_CASE(get_value_bool_recognises_true_and_1_as_true) { ECF_NAME_THIS_TEST(); BOOST_CHECK(ecf::PTree(std::string("true")).get_value() == true); - BOOST_CHECK(ecf::PTree(std::string("1")).get_value() == true); + BOOST_CHECK(ecf::PTree(std::string("1")).get_value() == true); } BOOST_AUTO_TEST_CASE(get_value_bool_recognises_false_and_0_as_false) { ECF_NAME_THIS_TEST(); BOOST_CHECK(ecf::PTree(std::string("false")).get_value() == false); - BOOST_CHECK(ecf::PTree(std::string("0")).get_value() == false); + BOOST_CHECK(ecf::PTree(std::string("0")).get_value() == false); } BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_unrecognised_string) { ECF_NAME_THIS_TEST(); - BOOST_CHECK_THROW(ecf::PTree(std::string("yes")).get_value(), ecf::PTreeValueError); - BOOST_CHECK_THROW(ecf::PTree(std::string("no")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("yes")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("no")).get_value(), ecf::PTreeValueError); BOOST_CHECK_THROW(ecf::PTree(std::string("True")).get_value(), ecf::PTreeValueError); - BOOST_CHECK_THROW(ecf::PTree(std::string("")).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(std::string("")).get_value(), ecf::PTreeValueError); } BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_int_node) { ECF_NAME_THIS_TEST(); - BOOST_CHECK_THROW(ecf::PTree(1).get_value(), ecf::PTreeValueError); - BOOST_CHECK_THROW(ecf::PTree(0).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(1).get_value(), ecf::PTreeValueError); + BOOST_CHECK_THROW(ecf::PTree(0).get_value(), ecf::PTreeValueError); } BOOST_AUTO_TEST_CASE(get_value_bool_throws_PTreeValueError_for_null_node) {