From a0d5c4fa9ceaeeb0e0f3ea9a7447fb4ea170139d Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 29 Sep 2026 15:38:20 +1300 Subject: [PATCH 1/2] Fill the project database from the file as it is read The project database -- behind parts lists, summary tables and the wiring list -- was filled by walking the built folios: every element and conductor object of every scene. Filling it from the document the project was just read from instead is what the database needs before a project could be opened without building every folio (qelectrotech-docker DB-FROM-XML-SCOPE.md). updateDB(document), called by readProjectXml(), fills diagram, diagram_info, element, element_info, terminal and conductor from the document, through the code the folios use: a BorderTitleBlock read from each folio's XML gives the title-block values and each element's grid cell, the embedded collection gives each definition, and the binders are shared with the live path. Shapes, texts and pictures still come from the folios (their boxes need fonts and pens). It falls back to the folios, saying why in the log, when the document does not carry what that needs: an item without a saved uuid (older files -- the folios derive them on load), a conductor naming its ends the older way, a %autonum folio number, a terminal showing its master's label, a missing or unbuildable definition. A symbol label computed from a formula is the one saved in the file, which QElectroTech writes as it computes it on every save. QET_DATABASE_FROM_FOLIOS=1 forces the folio path. tst_databasefromdocument saves every example once, then fills both ways and requires identical tables (24/24, filled from the document each time), and checks that an older file falls back with its reason. Red when the document path is made to write a wrong grid cell. Database phase of loading unchanged: industrial.qet 0.146 s vs 0.160 s, Polonez 0.040 s vs 0.038 s (median of 5). QETProject::projectWideProperties() is split out of updateDiagramsFolioData() so both fills use the same title-block context. Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/dataBase/projectdatabase.cpp | 453 +++++++++++++++++++++- sources/dataBase/projectdatabase.h | 21 + sources/qetproject.cpp | 19 +- sources/qetproject.h | 1 + tests/qttest/CMakeLists.txt | 13 + tests/qttest/tst_databasefromdocument.cpp | 131 +++++++ 6 files changed, 627 insertions(+), 11 deletions(-) create mode 100644 tests/qttest/tst_databasefromdocument.cpp diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 865deab89..6e09e8739 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -19,6 +19,7 @@ #include "sqlreadonly.h" +#include "../bordertitleblock.h" #include "../diagram.h" #include "../diagramposition.h" #include "../elementprovider.h" @@ -32,8 +33,15 @@ #include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../qetproject.h" +#include "../qet.h" +#include "../ElementsCollection/xmlelementcollection.h" +#include "../properties/elementdata.h" #include +#include +#include + +#include #include #include #include @@ -125,6 +133,389 @@ void projectDataBase::updateDB() emit dataBaseUpdated(); } +/** + @brief projectDataBase::updateDB + updateDB() for a project just read from @p document. + + The diagram, diagram_info, element, element_info, terminal and conductor + tables are filled from the document itself when it carries everything + they need -- see populateFromDocument() -- and from the built folios + otherwise, as updateDB() does. Shapes, texts and pictures always come from + the built folios: their boxes need the fonts and pens a folio renders with. + + The two fills give the same tables (tst_databasefromdocument checks it on + the shipped examples). Reading the document instead of the folios is what + the database needs before a project can be opened without building every + folio; see DB-FROM-XML-SCOPE.md in qelectrotech-docker. +*/ +void projectDataBase::updateDB(const QDomDocument &document) +{ + if (m_update_blocked || !m_content_changed) { + updateDB(); + return; + } + //QET_DATABASE_FROM_FOLIOS=1 keeps the fill from the built folios: + //a way back should the two ever disagree, and what the test that + //says they do not compares against. + QString why; + if (qEnvironmentVariableIntValue("QET_DATABASE_FROM_FOLIOS") == 1) { + why = QStringLiteral("QET_DATABASE_FROM_FOLIOS is set"); + } else if (populateFromDocument(document, &why)) { + qInfo() << "Project database filled from the document"; + populateDrawingItemTables(); + m_content_changed = false; + emit dataBaseUpdated(); + return; + } + qInfo().noquote() << "Project database filled from the folios:" << why; + updateDB(); +} + +namespace { +struct DocumentTerminal +{ + QString uuid; + QString name; + bool master_label = false; +}; + +struct DocumentDefinition +{ + QString type; + QString sub_type; + QHash terminals; +}; + +struct DocumentElement +{ + QString uuid; + QString diagram_uuid; + QString pos; + QString type; + QString sub_type; + QVariant group; + DiagramContext informations; + QHash terminals; +}; + +struct DocumentConductor +{ + QString uuid; + QString diagram_uuid; + QString element1, terminal1, element2, terminal2; + QString text; +}; +} + +/** + @brief projectDataBase::populateFromDocument + Fill the diagram, diagram_info, element, element_info, terminal and + conductor tables from @p document, the project as read from its file, + with the same values the built folios give -- using the same code: + a BorderTitleBlock read from each folio's XML gives the title-block + values and each element's grid cell, the project's embedded collection + gives each element's definition, and the binders are shared. + + Nothing is written unless the whole document can be read this way. It + cannot -- and false is returned, for the caller to fill from the folios + instead -- when a folio, element or conductor carries no saved uuid (the + folios derive one on load), a conductor names its ends the older way, + a folio number uses %autonum, a conductor ends on a terminal that shows + its master's contact label, or an element's definition is missing or + not one the folios could build. A file saved by a current QElectroTech + carries everything else. + + A symbol label computed from a formula is the label saved in the file, + which QElectroTech writes as it computes it on every save. + @return true if the tables were filled +*/ +bool projectDataBase::populateFromDocument(const QDomDocument &document, QString *why) +{ + auto refuse = [why](const QString &reason) { + if (why) *why = reason; + return false; + }; + if (!m_project) { + return refuse(QStringLiteral("no project")); + } + + //The folios, in the order the project read them (readDiagramsXml()), + //each with its saved uuid -- the one the built folio has. + const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram")); + const QList diagrams = m_project->diagrams(); + if (diagram_nodes.size() != diagrams.size()) { + return refuse(QStringLiteral("the folios are not the document's")); + } + + const DiagramContext project_wide = m_project->projectWideProperties(); + //One border and title block read from each folio's XML in turn: + //only what it gives is kept. + BorderTitleBlock reader; + QList diagram_infos; + QList diagram_dates; + QList diagram_uuids; + QList elements; + QList conductors; + QSet element_uuids; + QHash definitions; //by element type + + //The project's own definitions by "embed://" path, which is how an + //element's type names them: one walk of the embedded collection + //rather than resolving an ElementsLocation per type. + QHash stored; + std::function walk = + [&stored, &walk](const QDomElement &category, const QString &path) { + for (QDomElement child = category.firstChildElement() ; + !child.isNull() ; child = child.nextSiblingElement()) { + const QString name = path + child.attribute(QStringLiteral("name")); + if (child.tagName() == QLatin1String("category")) { + walk(child, name + QLatin1Char('/')); + } else if (child.tagName() == QLatin1String("element")) { + stored.insert(name, child.firstChildElement(QStringLiteral("definition"))); + } + } + }; + if (auto collection = m_project->embeddedElementCollection()) { + walk(collection->root(), QStringLiteral("embed://")); + } + + for (int i = 0 ; i < diagram_nodes.size() ; ++i) + { + const QDomElement diagram_xml = diagram_nodes.at(i).toElement(); + const QUuid diagram_uuid(diagram_xml.attribute(QStringLiteral("uuid"))); + if (diagram_uuid.isNull() || diagram_uuid != diagrams.at(i)->uuid()) { + return refuse(QStringLiteral("a folio has no saved uuid")); + } + + BorderTitleBlock *border = &reader; + border->titleBlockFromXml(diagram_xml); + border->borderFromXml(diagram_xml); + if (border->folio().contains(QStringLiteral("%autonum"))) { + return refuse(QStringLiteral("a folio number uses %autonum")); + } + border->setFolioData(i + 1, int(diagram_nodes.size()), QString(), project_wide); + + QHash on_this_folio; //element uuid -> index in elements + for (const QDomElement &element_xml : QET::findInDomElement( + diagram_xml, QStringLiteral("elements"), QStringLiteral("element"))) + { + const QUuid uuid(element_xml.attribute(QStringLiteral("uuid"))); + if (uuid.isNull() || element_uuids.contains(uuid)) { + return refuse(QStringLiteral("an element has no saved uuid, or shares one")); + } + element_uuids.insert(uuid); + + //The definition the folio builds the element from, with the + //checks Element::buildFromXml() refuses an element on -- + //read once per type, as many elements share one. + const QString type = element_xml.attribute(QStringLiteral("type")); + auto known = definitions.constFind(type); + if (known == definitions.constEnd()) + { + const QDomElement definition = stored.value(type); + if (definition.isNull()) { + return refuse(QStringLiteral("an element's definition is not in the project")); + } + int number; + if (definition.tagName() != QLatin1String("definition") + || definition.attribute(QStringLiteral("type")) != QLatin1String("element") + || !QET::attributeIsAnInteger(definition, QStringLiteral("width"), &number) + || !QET::attributeIsAnInteger(definition, QStringLiteral("height"), &number) + || !QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_x"), &number) + || !QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_y"), &number) + || definition.firstChild().isNull()) { + return refuse(QStringLiteral("an element's definition cannot be built")); + } + DocumentDefinition read; + ElementData data; + data.fromXml(definition); + read.type = data.typeToString(); + read.sub_type = data.masterTypeToString(); + const QDomElement description = definition.firstChildElement(QStringLiteral("description")); + for (QDomElement t = description.firstChildElement(QStringLiteral("terminal")) ; + !t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal"))) + { + const QUuid terminal_uuid(t.attribute(QStringLiteral("uuid"))); + if (terminal_uuid.isNull()) { + continue; + } + DocumentTerminal terminal; + terminal.uuid = terminal_uuid.toString(); + terminal.name = t.attribute(QStringLiteral("name")); + terminal.master_label = t.attribute(QStringLiteral("use_master_label")) == QLatin1String("true"); + read.terminals.insert(terminal_uuid, terminal); + } + known = definitions.insert(type, read); + } + + DocumentElement element; + element.uuid = uuid.toString(); + element.diagram_uuid = diagram_uuid.toString(); + element.pos = border->convertPosition( + QPointF(element_xml.attribute(QStringLiteral("x")).toDouble(), + element_xml.attribute(QStringLiteral("y")).toDouble())) + .toString(); + element.type = known->type; + element.sub_type = known->sub_type; + element.terminals = known->terminals; + const QUuid group = ItemGroups::read(element_xml); + element.group = group.isNull() ? QVariant() : QVariant(group.toString()); + element.informations.fromXml( + element_xml.firstChildElement(QStringLiteral("elementInformations")), + QStringLiteral("elementInformation")); + + on_this_folio.insert(uuid, int(elements.size())); + elements << element; + } + + //The conductors the folio builds, and only those: an end that is + //not found, a conductor from a terminal to itself and a second + //conductor between the same two terminals are all dropped on load. + QSet joined; + for (QDomElement conductor_xml : QET::findInDomElement( + diagram_xml, QStringLiteral("conductors"), QStringLiteral("conductor"))) + { + if (!Conductor::valideXml(conductor_xml)) { + continue; + } + const QUuid uuid(conductor_xml.attribute(QStringLiteral("uuid"))); + if (uuid.isNull() + || !conductor_xml.hasAttribute(QStringLiteral("element1")) + || !conductor_xml.hasAttribute(QStringLiteral("element2"))) { + return refuse(QStringLiteral("a conductor has no saved uuid, or names its ends the older way")); + } + QString ends[2][2]; + bool found = true; + for (int n = 0 ; n < 2 ; ++n) + { + const QString index = QString::number(n + 1); + const QUuid owner(conductor_xml.attribute(QStringLiteral("element") + index)); + const QUuid terminal(conductor_xml.attribute(QStringLiteral("terminal") + index)); + const int e = on_this_folio.value(owner, -1); + if (e < 0) { + found = false; + break; + } + if (!elements.at(e).terminals.contains(terminal)) { + //The folio would try the terminal's derived uuid; + //leave that to it. + return refuse(QStringLiteral("a conductor ends on a terminal not in its element's definition")); + } + const DocumentTerminal &t = elements.at(e).terminals.value(terminal); + if (t.master_label) { + return refuse(QStringLiteral("a conductor ends on a terminal showing its master's label")); + } + ends[n][0] = elements.at(e).uuid; + ends[n][1] = t.uuid; + } + if (!found) { + continue; + } + const QString a = ends[0][0] + ends[0][1], b = ends[1][0] + ends[1][1]; + if (a == b) { + continue; + } + const QString pair = a < b ? a + b : b + a; + if (joined.contains(pair)) { + continue; + } + joined.insert(pair); + + DocumentConductor conductor; + conductor.uuid = uuid.toString(); + conductor.diagram_uuid = diagram_uuid.toString(); + conductor.element1 = ends[0][0]; + conductor.terminal1 = ends[0][1]; + conductor.element2 = ends[1][0]; + conductor.terminal2 = ends[1][1]; + //ConductorProperties::fromXml()'s text, without reading the + //rest of the properties + conductor.text = conductor_xml.attribute(QStringLiteral("num")); + conductors << conductor; + } + + diagram_uuids << diagram_uuid; + diagram_infos << border->titleblockInformation(); + diagram_dates << border->date(); + } + + //Everything could be read: write it. + QSqlQuery query(m_data_base); + for (const QString &table : {QStringLiteral("diagram"), QStringLiteral("diagram_info"), + QStringLiteral("element"), QStringLiteral("element_info"), + QStringLiteral("conductor"), QStringLiteral("terminal")}) { + query.exec(QStringLiteral("DELETE FROM ") + table); + } + + for (int i = 0 ; i < diagram_uuids.size() ; ++i) + { + m_insert_diagram_query.bindValue(":uuid", diagram_uuids.at(i).toString()); + m_insert_diagram_query.bindValue(":pos", i + 1); + if (!m_insert_diagram_query.exec()) { + qDebug() << "projectDataBase::populateFromDocument diagram insert error : " << m_insert_diagram_query.lastError(); + } + bindDiagramInfoValues(m_insert_diagram_info_query, diagram_uuids.at(i), + diagram_infos.at(i), diagram_dates.at(i)); + if (!m_insert_diagram_info_query.exec()) { + qDebug() << "projectDataBase::populateFromDocument diagram_info insert error : " << m_insert_diagram_info_query.lastError(); + } + } + + for (const DocumentElement &element : std::as_const(elements)) + { + m_insert_elements_query.bindValue(QStringLiteral(":uuid"), element.uuid); + m_insert_elements_query.bindValue(QStringLiteral(":diagram_uuid"), element.diagram_uuid); + m_insert_elements_query.bindValue(QStringLiteral(":pos"), element.pos); + m_insert_elements_query.bindValue(QStringLiteral(":type"), element.type); + m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_type); + m_insert_elements_query.bindValue(QStringLiteral(":group_uuid"), element.group); + if (!m_insert_elements_query.exec()) { + qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError(); + } + bindElementInfoValues(m_insert_element_info_query, element.uuid, element.informations, + element.informations[QStringLiteral("label")].toString()); + if (!m_insert_element_info_query.exec()) { + qDebug() << "projectDataBase::populateFromDocument element_info insert error : " << m_insert_element_info_query.lastError(); + } + } + + QHash element_index; + for (int i = 0 ; i < elements.size() ; ++i) { + element_index.insert(elements.at(i).uuid, i); + } + for (const DocumentConductor &conductor : std::as_const(conductors)) + { + for (const auto &end : {std::make_pair(conductor.element1, conductor.terminal1), + std::make_pair(conductor.element2, conductor.terminal2)}) { + const DocumentElement &owner = elements.at(element_index.value(end.first)); + insertTerminal(end.second, end.first, + owner.terminals.value(QUuid(end.second)).name); + } + m_insert_conductor_query.bindValue(QStringLiteral(":uuid"), conductor.uuid); + m_insert_conductor_query.bindValue(QStringLiteral(":diagram_uuid"), conductor.diagram_uuid); + m_insert_conductor_query.bindValue(QStringLiteral(":terminal1_uuid"), conductor.terminal1); + m_insert_conductor_query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor.element1); + m_insert_conductor_query.bindValue(QStringLiteral(":terminal2_uuid"), conductor.terminal2); + m_insert_conductor_query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor.element2); + m_insert_conductor_query.bindValue(QStringLiteral(":text"), conductor.text); + if (!m_insert_conductor_query.exec()) { + qDebug() << "projectDataBase::populateFromDocument conductor insert error : " << m_insert_conductor_query.lastError(); + } + } + + //While every folio is still built, their conductors are watched for + //property changes as populateConductorTable() does -- the one walk + //over the built folios left here. + for (Diagram *diagram : diagrams) { + for (Conductor *conductor : diagram->conductors()) { + if (conductor->terminal1->parentElement() && conductor->terminal2->parentElement()) { + watchConductor(conductor); + } + } + } + return true; +} + /** @brief projectDataBase::setUpdateBlocked @param blocked : whether updateDB() should skip the full rebuild @@ -1514,9 +1905,21 @@ void projectDataBase::populateConductorTable() */ void projectDataBase::insertTerminal(Terminal *terminal) { - m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString()); - m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString()); - m_insert_terminal_query.bindValue(":name", terminal->name()); + insertTerminal(terminal->stableUuid().toString(), + terminal->parentElement()->uuid().toString(), + terminal->name()); +} + +/** + @brief projectDataBase::insertTerminal + insertTerminal(Terminal *) from values rather than a live terminal. +*/ +void projectDataBase::insertTerminal(const QString &uuid, const QString &element_uuid, + const QString &name) +{ + m_insert_terminal_query.bindValue(":uuid", uuid); + m_insert_terminal_query.bindValue(":element_uuid", element_uuid); + m_insert_terminal_query.bindValue(":name", name); if (!m_insert_terminal_query.exec()) { qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError(); } @@ -1733,11 +2136,48 @@ void projectDataBase::bindElementInfoValues(QSqlQuery &query, Element *element) } } +/** + @brief projectDataBase::bindElementInfoValues + bindElementInfoValues(QSqlQuery &, Element *) from values: the element's + information and the label to store for it. +*/ +void projectDataBase::bindElementInfoValues(QSqlQuery &query, const QString &element_uuid, + const DiagramContext &informations, + const QString &label) +{ + query.bindValue(QStringLiteral(":uuid"), element_uuid); + for (const auto &key : QETInformation::elementInfoKeys()) { + query.bindValue(QStringLiteral(":") + key, + key == QLatin1String("label") ? label + : informations[key].toString()); + } +} + void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram) { - query.bindValue(":uuid", diagram->uuid()); + bindDiagramInfoValues(query, diagram->uuid(), diagram->border_and_titleblock); +} + +/** + @brief projectDataBase::bindDiagramInfoValues + bindDiagramInfoValues(QSqlQuery &, Diagram *) from a folio's uuid and its + border and title block, which need not belong to a built folio. +*/ +void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid, + const BorderTitleBlock &border) +{ + bindDiagramInfoValues(query, diagram_uuid, border.titleblockInformation(), border.date()); +} + +/** + @brief projectDataBase::bindDiagramInfoValues + The same from a title block's information and date. +*/ +void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid, + const DiagramContext &infos, const QDate &date) +{ + query.bindValue(":uuid", diagram_uuid); - auto infos = diagram->border_and_titleblock.titleblockInformation(); for (auto key : QETInformation::diagramInfoKeys()) { if (key == "date") { @@ -1745,8 +2185,7 @@ void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram) //back: that text is the locale's short format, and where //it has a two-digit year (en_US "M/d/yy") toDate() reads //2010 back as 1910. - query.bindValue(QStringLiteral(":date"), - diagram->border_and_titleblock.date()); + query.bindValue(QStringLiteral(":date"), date); } else { auto value = infos.value(key); auto bind = key.prepend(":"); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 510d17eb7..67472af91 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -33,6 +33,10 @@ class Diagram; class Conductor; class Terminal; class QGraphicsItem; +class QDomDocument; +class BorderTitleBlock; +class DiagramContext; +class QDate; /** @brief The projectDataBase class @@ -50,6 +54,13 @@ class projectDataBase : public QObject virtual ~projectDataBase() override; void updateDB(); + /** + updateDB() for a project just read from @p document: the + diagram, element, terminal and conductor tables are filled + from the document when it carries everything they need (see + populateFromDocument()), from the built folios otherwise. + */ + void updateDB(const QDomDocument &document); /** Suppress the full rebuild performed by updateDB(). @@ -131,18 +142,28 @@ class projectDataBase : public QObject void populateDiagramInfoTable(); void populateConductorTable(); void populateDrawingItemTables(); + bool populateFromDocument(const QDomDocument &document, QString *why = nullptr); bool writeDrawingItem(QObject *object); void flushDrawingItems(); void forgetDrawingItem(QObject *object); void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram); void watchConductor(Conductor *conductor); void insertTerminal(Terminal *terminal); + void insertTerminal(const QString &uuid, const QString &element_uuid, + const QString &name); void prepareQuery(); static QHash elementInfoToString( Element *elmt); void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram); + static void bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid, + const BorderTitleBlock &border); + static void bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid, + const DiagramContext &infos, const QDate &date); static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram); static void bindElementInfoValues(QSqlQuery &query, Element *element); + static void bindElementInfoValues(QSqlQuery &query, const QString &element_uuid, + const DiagramContext &informations, + const QString &label); private: QPointer m_project; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index aae5a8bae..9cb4e3e33 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1880,7 +1880,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project) m_data_base.blockSignals(false); m_data_base.setUpdateBlocked(false); - m_data_base.updateDB(); + m_data_base.updateDB(xml_project); const qint64 database_ms = phase_timer.elapsed(); qInfo().nospace() @@ -2473,14 +2473,25 @@ bool QETProject::projectWasModified() Indique a chaque schema du projet quel est son numero de folio et combien de folio le projet contient. */ -void QETProject::updateDiagramsFolioData() +/** + @brief QETProject::projectWideProperties + @return the project's properties as every folio's title block sees them: + the user's project properties plus the project's title, path and file name. +*/ +DiagramContext QETProject::projectWideProperties() { - int total_folio = m_diagrams_list.count(); - DiagramContext project_wide_properties = m_project_properties; project_wide_properties.addValue("projecttitle", title()); project_wide_properties.addValue("projectpath", filePath()); project_wide_properties.addValue("projectfilename", QFileInfo(filePath()).baseName()); + return project_wide_properties; +} + +void QETProject::updateDiagramsFolioData() +{ + int total_folio = m_diagrams_list.count(); + + const DiagramContext project_wide_properties = projectWideProperties(); for (int i = 0 ; i < total_folio ; ++ i) { diff --git a/sources/qetproject.h b/sources/qetproject.h index 35830912c..b30b07075 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -221,6 +221,7 @@ class QETProject : public QObject bool projectWasModified(); bool projectOptionsWereModified(); DiagramContext projectProperties(); + DiagramContext projectWideProperties(); void setProjectProperties(const DiagramContext &); QUndoStack* undoStack() {return m_undo_stack;} diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 1d37ab8d8..de6de9667 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -344,6 +344,19 @@ if(QET_HAS_SCRIPTING) target_compile_definitions(tst_scriptconductoruuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"") + # The project database filled from the file as it is read holds exactly + # what it holds filled from the built folios (QET_DATABASE_FROM_FOLIOS=1), + # on every example saved once; an older file falls back, saying why. + add_executable( + tst_databasefromdocument + tst_databasefromdocument.cpp) + add_test(NAME tst_databasefromdocument COMMAND tst_databasefromdocument) + add_dependencies(tst_databasefromdocument qelectrotech) + target_link_libraries(tst_databasefromdocument PRIVATE Qt::Test) + target_compile_definitions(tst_databasefromdocument PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # The project database keeps a folio's date with its century: 741.qet's # 2010-09-21, read through project_summary_view under LC_ALL=en_US.UTF-8. add_executable( diff --git a/tests/qttest/tst_databasefromdocument.cpp b/tests/qttest/tst_databasefromdocument.cpp new file mode 100644 index 000000000..f375dd2da --- /dev/null +++ b/tests/qttest/tst_databasefromdocument.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// A project's database, filled from the file as it is read, holds exactly +// what the same database filled from the built folios holds. Every example +// is saved once (so that it carries the uuids a current QElectroTech +// writes), then opened twice through the real binary's --run: once as is, +// once with QET_DATABASE_FROM_FOLIOS=1, and the six tables compared. +class tst_databasefromdocument : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + int m_run = 0; + + QProcessEnvironment env() + { + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++)); + QDir().mkpath(home); + QProcessEnvironment e = QProcessEnvironment::systemEnvironment(); + e.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + e.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + e.insert(QStringLiteral("HOME"), home); + e.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + e.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + e.insert(QStringLiteral("TMPDIR"), m_dir.path()); + e.remove(QStringLiteral("QET_DATABASE_FROM_FOLIOS")); + return e; + } + + QString run(const QStringList &args, bool from_folios = false) + { + QProcessEnvironment e = env(); + if (from_folios) + e.insert(QStringLiteral("QET_DATABASE_FROM_FOLIOS"), QStringLiteral("1")); + QProcess proc; + proc.setProcessEnvironment(e); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args); + if (!proc.waitForFinished(180000)) return {}; + return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); + } + + // The six tables, each as a sorted list of its rows, and which way the + // database was filled. + QJsonObject dump(const QString &project, bool from_folios, QString *how) + { + const QString out = run({QStringLiteral("--run"), m_dir.filePath(QStringLiteral("dump.js")), + project}, from_folios); + QJsonObject tables; + for (const QString &line : out.split(QLatin1Char('\n'))) { + if (line.contains(QStringLiteral("Project database filled"))) + *how = line.mid(line.indexOf(QStringLiteral("Project database filled"))); + const int i = line.indexOf(QStringLiteral("DUMP ")); + if (i >= 0) { + const QJsonObject raw = QJsonDocument::fromJson(line.mid(i + 5).toUtf8()).object(); + for (auto it = raw.begin(); it != raw.end(); ++it) { + QStringList rows; + for (const QJsonValue &row : it.value().toArray()) + rows << QString::fromUtf8(QJsonDocument(row.toObject()).toJson(QJsonDocument::Compact)); + rows.sort(); + tables.insert(it.key(), QJsonArray::fromStringList(rows)); + } + } + } + return tables; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + QFile js(m_dir.filePath(QStringLiteral("dump.js"))); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write("var out = {};\n" + "['diagram', 'diagram_info', 'element', 'element_info', 'terminal', 'conductor']" + ".forEach(function (t) { out[t] = qet.query('SELECT * FROM ' + t); });\n" + "qet.log('DUMP ' + JSON.stringify(out));\n"); + } + + void sameTablesBothWays_data() + { + QTest::addColumn("project"); + const QDir examples(QStringLiteral(QET_EXAMPLES_DIR)); + for (const QString &f : examples.entryList({QStringLiteral("*.qet")}, QDir::Files, QDir::Name)) + QTest::newRow(f.toUtf8().constData()) << examples.filePath(f); + } + + void sameTablesBothWays() + { + QFETCH(QString, project); + const QString saved = m_dir.filePath(QStringLiteral("saved%1.qet").arg(m_run)); + run({QStringLiteral("--resave"), project, saved}); + QVERIFY2(QFile::exists(saved), "--resave failed"); + + QString how_document, how_folios; + const QJsonObject document = dump(saved, false, &how_document); + const QJsonObject folios = dump(saved, true, &how_folios); + QVERIFY2(how_document == QLatin1String("Project database filled from the document"), + qPrintable(how_document)); + QVERIFY2(how_folios.contains(QStringLiteral("QET_DATABASE_FROM_FOLIOS")), qPrintable(how_folios)); + QCOMPARE(document.keys().size(), 6); + for (const QString &table : folios.keys()) { + const QJsonArray a = document.value(table).toArray(), b = folios.value(table).toArray(); + QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows from the document, %3 from the folios") + .arg(table).arg(a.size()).arg(b.size()))); + } + } + + // A file whose items carry no saved uuid is filled from the folios, + // which work those uuids out as they are built, and says why. + void olderFileFallsBack() + { + QString how; + dump(QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), false, &how); + QCOMPARE(how, QStringLiteral("Project database filled from the folios: a folio has no saved uuid")); + } +}; + +QTEST_APPLESS_MAIN(tst_databasefromdocument) + +#include "tst_databasefromdocument.moc" From b1abab1241cb2330d74a020db9434dec9c1e637b Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 29 Sep 2026 16:08:45 +1300 Subject: [PATCH 2/2] Work out formula labels and wire texts from the file, with the folios' code The first fill from the document stored the label and wire text saved in the file. QElectroTech saves what it last worked out, not what the formula gives now: K%total-%id saved as K1-1 on a 3-folio project is K3-1 on the folios, and the same for a wire's formula. So the document fill disagreed with the folio fill on any file whose folios had been added or moved. AssignVariables now works from a FormulaContext: the folio's number, index, total, plant, location, title-block and project variables, and the element's grid cell and prefix or the wire's four properties. The Diagram overload fills one from a built folio, the database fills one from the file, and both call the same evaluation. Also brought in line with Diagram::fromXml(): an element Element::valideXml() rejects is skipped with its wires; a folio where two elements number their terminals alike (Element::fromXml() refuses one, by geometry), a frozen formula wire text, and the older sequential-number attributes are left to the folios. Each folio's border now starts from the defaults a new Diagram has, not the previous folio's values. tst_databasefromdocument: formulasAreWorkedOut (every variable kind, stale saved values), unbuiltElementIsLeftOut, clashingTerminalIdsFallBack. Each fails with its fix removed. Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/autoNum/assignvariables.cpp | 153 ++++++++++-------- sources/autoNum/assignvariables.h | 38 ++++- sources/dataBase/projectdatabase.cpp | 120 ++++++++++++-- tests/qttest/CMakeLists.txt | 2 +- tests/qttest/tst_databasefromdocument.cpp | 186 ++++++++++++++++++++-- 5 files changed, 405 insertions(+), 94 deletions(-) diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp index c8059bbf2..1cd48953e 100644 --- a/sources/autoNum/assignvariables.cpp +++ b/sources/autoNum/assignvariables.cpp @@ -220,12 +220,56 @@ namespace autonum Diagram *diagram, const Element *elmt, const Conductor *cndr) + { + if (!diagram) { + return formula; + } + + FormulaContext context; + const BorderTitleBlock &border = diagram->border_and_titleblock; + context.folio = border.folio(); + context.folio_index = diagram->folioIndex(); + context.folio_total = border.folioTotal(); + context.plant = border.plant(); + context.locmach = border.locmach(); + context.title_block_fields = border.additionalFields(); + context.project_properties = diagram->project()->projectProperties(); + if (elmt) + { + context.has_element = true; + context.element_position = diagram->convertPosition(elmt->scenePos()); + context.element_prefix = elmt->getPrefix(); + } + if (cndr) + { + context.has_conductor = true; + context.wire_function = cndr->properties().m_function; + context.wire_tension_protocol = cndr->properties().m_tension_protocol; + context.wire_color = cndr->properties().m_wire_color; + context.wire_section = cndr->properties().m_wire_section; + } + return formulaToLabel(std::move(formula), seqStruct, context); + } + + /** + @brief AssignVariables::formulaToLabel + Return the formula with its variables assigned from @p context, + which describes a folio and the element or conductor the formula + belongs to without needing either to be built. + @param formula - the formula to work + @param seqStruct - struct where is stocked int values + (struct is passed as a reference + and modified by this static method) + @param context - what the variables are read from + @return the string with variable assigned. + */ + QString AssignVariables::formulaToLabel(QString formula, + sequentialNumbers &seqStruct, + const FormulaContext &context) { AssignVariables av(std::move(formula), seqStruct, - diagram, - elmt, - cndr); + context); seqStruct = av.m_seq_struct; return av.m_assigned_label; } @@ -347,76 +391,54 @@ namespace autonum AssignVariables::AssignVariables(const QString& formula, const sequentialNumbers& seqStruct, - Diagram *diagram, - const Element *elmt, - const Conductor *cndr): - m_diagram(diagram), + const FormulaContext &context): + m_context(context), m_arg_formula(formula), m_assigned_label(formula), - m_seq_struct(seqStruct), - m_element(elmt), - m_conductor(cndr) + m_seq_struct(seqStruct) { - if (m_diagram) + m_assigned_label.replace("%F", m_context.folio); + m_assigned_label.replace("%f", + QString::number(m_context.folio_index+1)); + m_assigned_label.replace("%id", + QString::number(m_context.folio_index+1)); + m_assigned_label.replace("%total", + QString::number(m_context.folio_total)); + m_assigned_label.replace("%M", m_context.plant); + m_assigned_label.replace("%LM", m_context.locmach); + + QSettings settings; + if (m_context.has_element) { - m_assigned_label.replace("%F", - m_diagram - -> border_and_titleblock - .folio()); - m_assigned_label.replace("%f", - QString::number( - m_diagram - ->folioIndex()+1)); - m_assigned_label.replace("%id", - QString::number( - m_diagram - ->folioIndex()+1)); - m_assigned_label.replace("%total", - QString::number( - m_diagram - ->border_and_titleblock - .folioTotal())); - m_assigned_label.replace("%M", - m_diagram - -> border_and_titleblock - .plant()); - m_assigned_label.replace("%LM", - m_diagram - -> border_and_titleblock - .locmach()); - - QSettings settings; - if (m_element) - { - if (settings.value("border-columns_0", true).toBool()){ - m_assigned_label.replace("%c", QString::number(m_diagram->convertPosition(m_element->scenePos()).number() - 1)); - }else{ - m_assigned_label.replace("%c", QString::number(m_diagram->convertPosition(m_element->scenePos()).number())); - } - m_assigned_label.replace("%l", m_diagram->convertPosition(m_element->scenePos()).letter()); - m_assigned_label.replace("%prefix", m_element->getPrefix()); + if (settings.value("border-columns_0", true).toBool()){ + m_assigned_label.replace("%c", QString::number(m_context.element_position.number() - 1)); + }else{ + m_assigned_label.replace("%c", QString::number(m_context.element_position.number())); } - - if (m_conductor) - { - m_assigned_label.replace("%wf", cndr->properties().m_function); - m_assigned_label.replace("%wv", cndr->properties().m_tension_protocol); - m_assigned_label.replace("%wc", cndr->properties().m_wire_color); - m_assigned_label.replace("%ws", cndr->properties().m_wire_section); - } - - assignTitleBlockVar(); - assignProjectVar(); - assignSequence(); + m_assigned_label.replace("%l", m_context.element_position.letter()); + m_assigned_label.replace("%prefix", m_context.element_prefix); } + + if (m_context.has_conductor) + { + m_assigned_label.replace("%wf", m_context.wire_function); + m_assigned_label.replace("%wv", m_context.wire_tension_protocol); + m_assigned_label.replace("%wc", m_context.wire_color); + m_assigned_label.replace("%ws", m_context.wire_section); + } + + assignTitleBlockVar(); + assignProjectVar(); + assignSequence(); } void AssignVariables::assignTitleBlockVar() { - for (int i = 0; i < m_diagram->border_and_titleblock.additionalFields().count(); i++) + DiagramContext fields = m_context.title_block_fields; + for (int i = 0; i < fields.count(); i++) { - QString folio_variable = m_diagram->border_and_titleblock.additionalFields().keys().at(i); - QVariant folio_value = m_diagram->border_and_titleblock.additionalFields().operator [](folio_variable); + QString folio_variable = fields.keys().at(i); + QVariant folio_value = fields[folio_variable]; if (m_assigned_label.contains(folio_variable)) { m_assigned_label.replace("%{" + folio_variable + "}", folio_value.toString()); @@ -427,10 +449,11 @@ namespace autonum void AssignVariables::assignProjectVar() { - for (int i = 0; i < m_diagram->project()->projectProperties().count(); i++) + DiagramContext properties = m_context.project_properties; + for (int i = 0; i < properties.count(); i++) { - QString folio_variable = m_diagram->project()->projectProperties().keys().at(i); - QVariant folio_value = m_diagram->project()->projectProperties().operator [](folio_variable); + QString folio_variable = properties.keys().at(i); + QVariant folio_value = properties[folio_variable]; if (m_assigned_label.contains(folio_variable)) { m_assigned_label.replace("%{" + folio_variable + "}", folio_value.toString()); diff --git a/sources/autoNum/assignvariables.h b/sources/autoNum/assignvariables.h index 5fffdf43f..255ad6003 100644 --- a/sources/autoNum/assignvariables.h +++ b/sources/autoNum/assignvariables.h @@ -18,6 +18,7 @@ #ifndef ASSIGNVARIABLES_H #define ASSIGNVARIABLES_H #include "../diagramcontext.h" +#include "../diagramposition.h" #include "numerotationcontext.h" #include @@ -57,6 +58,36 @@ namespace autonum QStringList alpha; }; + /** + @brief The FormulaContext struct + Everything a formula's variables are read from: the folio it is on + and, for an element's or a conductor's formula, that item. + AssignVariables::formulaToLabel() fills one from a built folio; the + project database fills one from the project's file. + */ + struct FormulaContext + { + /// border_and_titleblock.folio(), folioIndex(), folioTotal()... + QString folio; + int folio_index = 0; + int folio_total = 0; + QString plant; + QString locmach; + /// the title block's additional fields and the project's properties + DiagramContext title_block_fields; + DiagramContext project_properties; + /// an element's grid cell (%c, %l) and prefix + bool has_element = false; + DiagramPosition element_position; + QString element_prefix; + /// a conductor's %wf, %wv, %wc and %ws + bool has_conductor = false; + QString wire_function; + QString wire_tension_protocol; + QString wire_color; + QString wire_section; + }; + /** @brief The AssignVariables class This class assign variable of a formula string. @@ -66,21 +97,20 @@ namespace autonum { public: static QString formulaToLabel (QString formula, sequentialNumbers &seqStruct, Diagram *diagram, const Element *elmt = nullptr, const Conductor *cndr = nullptr); + static QString formulaToLabel (QString formula, sequentialNumbers &seqStruct, const FormulaContext &context); static QString replaceVariable (const QString &formula, const DiagramContext &dc); static QString genericXref (const Element *element); private: - AssignVariables(const QString& formula, const sequentialNumbers& seqStruct , Diagram *diagram, const Element *elmt = nullptr, const Conductor *cndr = nullptr); + AssignVariables(const QString& formula, const sequentialNumbers& seqStruct, const FormulaContext &context); void assignTitleBlockVar(); void assignProjectVar(); void assignSequence(); - Diagram *m_diagram = nullptr; + const FormulaContext &m_context; QString m_arg_formula; QString m_assigned_label; sequentialNumbers m_seq_struct; - const Element *m_element = nullptr; - const Conductor *m_conductor = nullptr; }; void setSequentialToList(QStringList &list, NumerotationContext &nc, const QString& type); diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 6e09e8739..bea454bd4 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -19,6 +19,8 @@ #include "sqlreadonly.h" +#include "../autoNum/assignvariables.h" +#include "../borderproperties.h" #include "../bordertitleblock.h" #include "../diagram.h" #include "../diagramposition.h" @@ -34,6 +36,7 @@ #include "../qetinformation.h" #include "../qetproject.h" #include "../qet.h" +#include "../titleblockproperties.h" #include "../ElementsCollection/xmlelementcollection.h" #include "../properties/elementdata.h" @@ -195,9 +198,24 @@ struct DocumentElement QString sub_type; QVariant group; DiagramContext informations; + QString label; QHash terminals; }; + //The sequential values an element or a conductor was saved with, as + //Element::fromXml() and Conductor::fromXml() read them -- false for + //the attributes files written before carry. +bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence) +{ + for (const char *name : {"sequ_1", "sequf_1", "seqt_1", "seqtf_1", "seqh_1"}) { + if (item.hasAttribute(QLatin1String(name))) { + return false; + } + } + sequence->fromXml(item.firstChildElement(QStringLiteral("sequentialNumbers"))); + return true; +} + struct DocumentConductor { QString uuid; @@ -221,12 +239,17 @@ struct DocumentConductor instead -- when a folio, element or conductor carries no saved uuid (the folios derive one on load), a conductor names its ends the older way, a folio number uses %autonum, a conductor ends on a terminal that shows - its master's contact label, or an element's definition is missing or - not one the folios could build. A file saved by a current QElectroTech - carries everything else. + its master's contact label, an element's definition is missing or + not one the folios could build, two elements on a folio number their + terminals alike, or sequential numbers are saved as the attributes + older files carry. A file saved by a current QElectroTech carries + everything else. - A symbol label computed from a formula is the label saved in the file, - which QElectroTech writes as it computes it on every save. + A label or a conductor text made from a formula is worked out again, + as the folios do, with the same AssignVariables code: the one saved + in the file is what the formula gave when it was saved, and a folio + added or moved since changes it. A frozen conductor text is left to the + folios, which work it out part-way through loading. @return true if the tables were filled */ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString *why) @@ -248,6 +271,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } const DiagramContext project_wide = m_project->projectWideProperties(); + const DiagramContext project_properties = m_project->projectProperties(); //One border and title block read from each folio's XML in turn: //only what it gives is kept. BorderTitleBlock reader; @@ -288,6 +312,9 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } BorderTitleBlock *border = &reader; + //As a new Diagram's border starts, before initFromXml() reads it + border->importBorder(BorderProperties()); + border->importTitleBlock(TitleBlockProperties()); border->titleBlockFromXml(diagram_xml); border->borderFromXml(diagram_xml); if (border->folio().contains(QStringLiteral("%autonum"))) { @@ -295,16 +322,49 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } border->setFolioData(i + 1, int(diagram_nodes.size()), QString(), project_wide); + //What a formula on this folio is worked out from, as + //AssignVariables::formulaToLabel() reads it off a built folio. + autonum::FormulaContext folio_context; + folio_context.folio = border->folio(); + folio_context.folio_index = i; + folio_context.folio_total = border->folioTotal(); + folio_context.plant = border->plant(); + folio_context.locmach = border->locmach(); + folio_context.title_block_fields = border->additionalFields(); + folio_context.project_properties = project_properties; + QHash on_this_folio; //element uuid -> index in elements - for (const QDomElement &element_xml : QET::findInDomElement( + QSet terminal_ids; //the older terminal ids used so far + for (QDomElement element_xml : QET::findInDomElement( diagram_xml, QStringLiteral("elements"), QStringLiteral("element"))) { + //Skipped by Diagram::fromXml() as well + if (!Element::valideXml(element_xml)) { + continue; + } const QUuid uuid(element_xml.attribute(QStringLiteral("uuid"))); if (uuid.isNull() || element_uuids.contains(uuid)) { return refuse(QStringLiteral("an element has no saved uuid, or shares one")); } element_uuids.insert(uuid); + //Element::fromXml() refuses an element whose terminals are + //numbered like those of one read before it on the folio; + //which of them it matches needs their geometry, so leave any + //such folio to the folios. A current file numbers them + //across the folio. + QSet ids; + for (QDomElement t : QET::findInDomElement( + element_xml, QStringLiteral("terminals"), QStringLiteral("terminal"))) { + if (Terminal::valideXml(t)) { + ids.insert(t.attribute(QStringLiteral("id")).toInt()); + } + } + if (terminal_ids.intersects(ids)) { + return refuse(QStringLiteral("two elements on a folio number their terminals alike")); + } + terminal_ids.unite(ids); + //The definition the folio builds the element from, with the //checks Element::buildFromXml() refuses an element on -- //read once per type, as many elements share one. @@ -351,10 +411,10 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString DocumentElement element; element.uuid = uuid.toString(); element.diagram_uuid = diagram_uuid.toString(); - element.pos = border->convertPosition( - QPointF(element_xml.attribute(QStringLiteral("x")).toDouble(), - element_xml.attribute(QStringLiteral("y")).toDouble())) - .toString(); + DiagramPosition position = border->convertPosition( + QPointF(element_xml.attribute(QStringLiteral("x")).toDouble(), + element_xml.attribute(QStringLiteral("y")).toDouble())); + element.pos = position.toString(); element.type = known->type; element.sub_type = known->sub_type; element.terminals = known->terminals; @@ -363,6 +423,21 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString element.informations.fromXml( element_xml.firstChildElement(QStringLiteral("elementInformations")), QStringLiteral("elementInformation")); + //Element::actualLabel() + const QString formula = element.informations.value(QStringLiteral("formula")).toString(); + if (formula.isEmpty()) { + element.label = element.informations.value(QStringLiteral("label")).toString(); + } else { + autonum::sequentialNumbers sequence; + if (!readSequence(element_xml, &sequence)) { + return refuse(QStringLiteral("an element's sequential numbers are saved the older way")); + } + autonum::FormulaContext context = folio_context; + context.has_element = true; + context.element_position = position; + context.element_prefix = element_xml.attribute(QStringLiteral("prefix")); + element.label = autonum::AssignVariables::formulaToLabel(formula, sequence, context); + } on_this_folio.insert(uuid, int(elements.size())); elements << element; @@ -429,8 +504,27 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString conductor.element2 = ends[1][0]; conductor.terminal2 = ends[1][1]; //ConductorProperties::fromXml()'s text, without reading the - //rest of the properties - conductor.text = conductor_xml.attribute(QStringLiteral("num")); + //rest of the properties -- or, as Conductor::refreshText() + //makes it, what its formula gives. + const QString formula = conductor_xml.attribute(QStringLiteral("formula")); + if (formula.isEmpty()) { + conductor.text = conductor_xml.attribute(QStringLiteral("num")); + } else { + autonum::sequentialNumbers sequence; + if (conductor_xml.attribute(QStringLiteral("freezeLabel")) == QLatin1String("true")) { + return refuse(QStringLiteral("a conductor's text made from a formula is frozen")); + } + if (!readSequence(conductor_xml, &sequence)) { + return refuse(QStringLiteral("a conductor's sequential numbers are saved the older way")); + } + autonum::FormulaContext context = folio_context; + context.has_conductor = true; + context.wire_function = conductor_xml.attribute(QStringLiteral("function")); + context.wire_tension_protocol = conductor_xml.attribute(QStringLiteral("tension_protocol")); + context.wire_color = conductor_xml.attribute(QStringLiteral("conductor_color")); + context.wire_section = conductor_xml.attribute(QStringLiteral("conductor_section")); + conductor.text = autonum::AssignVariables::formulaToLabel(formula, sequence, context); + } conductors << conductor; } @@ -473,7 +567,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError(); } bindElementInfoValues(m_insert_element_info_query, element.uuid, element.informations, - element.informations[QStringLiteral("label")].toString()); + element.label); if (!m_insert_element_info_query.exec()) { qDebug() << "projectDataBase::populateFromDocument element_info insert error : " << m_insert_element_info_query.lastError(); } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index de6de9667..d324fac74 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -352,7 +352,7 @@ if(QET_HAS_SCRIPTING) tst_databasefromdocument.cpp) add_test(NAME tst_databasefromdocument COMMAND tst_databasefromdocument) add_dependencies(tst_databasefromdocument qelectrotech) - target_link_libraries(tst_databasefromdocument PRIVATE Qt::Test) + target_link_libraries(tst_databasefromdocument PRIVATE Qt::Test Qt::Xml) target_compile_definitions(tst_databasefromdocument PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") diff --git a/tests/qttest/tst_databasefromdocument.cpp b/tests/qttest/tst_databasefromdocument.cpp index f375dd2da..d2febebf4 100644 --- a/tests/qttest/tst_databasefromdocument.cpp +++ b/tests/qttest/tst_databasefromdocument.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -74,6 +75,44 @@ class tst_databasefromdocument : public QObject return tables; } + // tremie_vibrante.qet saved once, as a document to change + QDomDocument resaved(const QString &saved) + { + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved}); + QFile file(saved); + QDomDocument document; + if (file.open(QIODevice::ReadOnly)) + document.setContent(&file); + return document; + } + + static bool write(const QString &path, const QDomDocument &document) + { + QFile file(path); + return file.open(QIODevice::WriteOnly | QIODevice::Truncate) + && file.write(document.toByteArray()) > 0; + } + + // Both fills of @p saved, which must come from the document, agree. + void compareBothWays(const QString &saved) + { + QString how_document, how_folios; + const QJsonObject document = dump(saved, false, &how_document); + const QJsonObject folios = dump(saved, true, &how_folios); + QVERIFY2(how_document == QLatin1String("Project database filled from the document"), + qPrintable(how_document)); + QVERIFY2(how_folios.contains(QStringLiteral("QET_DATABASE_FROM_FOLIOS")), qPrintable(how_folios)); + QCOMPARE(document.keys().size(), 6); + for (const QString &table : folios.keys()) { + const QJsonArray a = document.value(table).toArray(), b = folios.value(table).toArray(); + QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows from the document, %3 from the folios") + .arg(table).arg(a.size()).arg(b.size()))); + } + m_last = document; + } + + QJsonObject m_last; // the document's tables, from compareBothWays() + private slots: void initTestCase() { @@ -101,19 +140,144 @@ private slots: const QString saved = m_dir.filePath(QStringLiteral("saved%1.qet").arg(m_run)); run({QStringLiteral("--resave"), project, saved}); QVERIFY2(QFile::exists(saved), "--resave failed"); + compareBothWays(saved); + } - QString how_document, how_folios; - const QJsonObject document = dump(saved, false, &how_document); - const QJsonObject folios = dump(saved, true, &how_folios); - QVERIFY2(how_document == QLatin1String("Project database filled from the document"), - qPrintable(how_document)); - QVERIFY2(how_folios.contains(QStringLiteral("QET_DATABASE_FROM_FOLIOS")), qPrintable(how_folios)); - QCOMPARE(document.keys().size(), 6); - for (const QString &table : folios.keys()) { - const QJsonArray a = document.value(table).toArray(), b = folios.value(table).toArray(); - QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows from the document, %3 from the folios") - .arg(table).arg(a.size()).arg(b.size()))); + // A label or a conductor text made from a formula is what the formula + // gives now, not what it gave when the file was saved: every folio's + // first element and first conductor is given a formula using each kind + // of variable, and a saved label no formula gives. + void formulasAreWorkedOut() + { + const QString saved = m_dir.filePath(QStringLiteral("formulas-saved.qet")); + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved}); + QFile file(saved); + QVERIFY(file.open(QIODevice::ReadOnly)); + QDomDocument document; + QVERIFY(document.setContent(&file)); + file.close(); + + auto property = [&document](QDomElement parent, const QString &name, const QString &value) { + QDomElement properties = parent.firstChildElement(QStringLiteral("properties")); + if (properties.isNull()) + properties = parent.appendChild(document.createElement(QStringLiteral("properties"))).toElement(); + QDomElement p = document.createElement(QStringLiteral("property")); + p.setAttribute(QStringLiteral("name"), name); + p.appendChild(document.createTextNode(value)); + properties.appendChild(p); + }; + auto sequence = [&document](QDomElement item, const QString &unit) { + item.removeChild(item.firstChildElement(QStringLiteral("sequentialNumbers"))); + QDomElement s = document.createElement(QStringLiteral("sequentialNumbers")); + QDomElement u = document.createElement(QStringLiteral("unit")); + u.appendChild(document.createTextNode(unit)); + s.appendChild(u); + item.appendChild(s); + }; + + property(document.documentElement(), QStringLiteral("site"), QStringLiteral("S")); + const QDomNodeList diagrams = document.elementsByTagName(QStringLiteral("diagram")); + QCOMPARE(diagrams.size(), 3); + for (int i = 0 ; i < diagrams.size() ; ++i) + { + QDomElement diagram = diagrams.at(i).toElement(); + diagram.setAttribute(QStringLiteral("folio"), QStringLiteral("F%id")); + diagram.setAttribute(QStringLiteral("plant"), QStringLiteral("P")); + diagram.setAttribute(QStringLiteral("locmach"), QStringLiteral("L")); + property(diagram, QStringLiteral("zone"), QStringLiteral("Z%1").arg(i)); + + QDomElement element = diagram.firstChildElement(QStringLiteral("elements")) + .firstChildElement(QStringLiteral("element")); + QVERIFY(!element.isNull()); + element.setAttribute(QStringLiteral("prefix"), QStringLiteral("X")); + sequence(element, QStringLiteral("7")); + QDomElement informations = element.firstChildElement(QStringLiteral("elementInformations")); + if (informations.isNull()) + informations = element.appendChild(document.createElement(QStringLiteral("elementInformations"))).toElement(); + while (!informations.firstChild().isNull()) + informations.removeChild(informations.firstChild()); + for (const auto &info : {std::make_pair(QStringLiteral("formula"), + QStringLiteral("K%total-%f-%F-%M-%LM-%c%l-%prefix-%{zone}-%{site}-%sequ_1")), + std::make_pair(QStringLiteral("label"), QStringLiteral("OLD"))}) { + QDomElement e = document.createElement(QStringLiteral("elementInformation")); + e.setAttribute(QStringLiteral("name"), info.first); + e.setAttribute(QStringLiteral("show"), QStringLiteral("1")); + e.appendChild(document.createTextNode(info.second)); + informations.appendChild(e); + } + + QDomElement conductor = diagram.firstChildElement(QStringLiteral("conductors")) + .firstChildElement(QStringLiteral("conductor")); + QVERIFY(!conductor.isNull()); + conductor.setAttribute(QStringLiteral("formula"), QStringLiteral("W%total-%id-%wf-%{zone}-%sequ_1")); + conductor.setAttribute(QStringLiteral("function"), QStringLiteral("N")); + conductor.setAttribute(QStringLiteral("num"), QStringLiteral("OLD")); + sequence(conductor, QStringLiteral("3")); } + QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate)); + file.write(document.toByteArray()); + file.close(); + + compareBothWays(saved); + // ...and they were worked out, not left as saved + const QString info = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("element_info")).toArray()) + .toJson(QJsonDocument::Compact)); + const QString wires = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("conductor")).toArray()) + .toJson(QJsonDocument::Compact)); + QVERIFY2(info.contains(QStringLiteral("K3-1-F1-P-L-")), qPrintable(info.left(400))); + QVERIFY2(info.contains(QStringLiteral("-X-Z0-S-7")), qPrintable(info.left(400))); + QVERIFY2(wires.contains(QStringLiteral("W3-3-N-Z2-3")), qPrintable(wires.left(400))); + QVERIFY(!info.contains(QStringLiteral("OLD"))); + QVERIFY(!wires.contains(QStringLiteral("OLD"))); + } + + // An element the folio does not build, and so the conductors ending on + // it, are left out the same way. + void unbuiltElementIsLeftOut() + { + const QString saved = m_dir.filePath(QStringLiteral("unbuilt.qet")); + QDomDocument document = resaved(saved); + QDomElement conductor = document.elementsByTagName(QStringLiteral("conductor")).at(0).toElement(); + QVERIFY(!conductor.isNull()); + const QString uuid = conductor.attribute(QStringLiteral("element1")); + const QDomNodeList elements = document.elementsByTagName(QStringLiteral("element")); + bool found = false; + for (int i = 0 ; i < elements.size() ; ++i) { + QDomElement e = elements.at(i).toElement(); + if (e.attribute(QStringLiteral("uuid")) == uuid) { + e.setAttribute(QStringLiteral("x"), QStringLiteral("nan")); + found = true; + } + } + QVERIFY(found); + QVERIFY(write(saved, document)); + compareBothWays(saved); + const QString wires = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("conductor")).toArray()) + .toJson(QJsonDocument::Compact)); + QVERIFY(!wires.contains(conductor.attribute(QStringLiteral("uuid")))); + } + + // Two elements on a folio numbering their terminals alike: which one the + // folio then refuses depends on the terminals' geometry, so the folios + // fill the database. + void clashingTerminalIdsFallBack() + { + const QString saved = m_dir.filePath(QStringLiteral("clash.qet")); + QDomDocument document = resaved(saved); + const QDomElement diagram = document.elementsByTagName(QStringLiteral("diagram")).at(0).toElement(); + QDomElement first = diagram.firstChildElement(QStringLiteral("elements")).firstChildElement(QStringLiteral("element")); + QDomElement second = first.nextSiblingElement(QStringLiteral("element")); + const QString id = first.firstChildElement(QStringLiteral("terminals")) + .firstChildElement(QStringLiteral("terminal")).attribute(QStringLiteral("id")); + QDomElement terminal = second.firstChildElement(QStringLiteral("terminals")) + .firstChildElement(QStringLiteral("terminal")); + QVERIFY(!id.isEmpty() && !terminal.isNull()); + terminal.setAttribute(QStringLiteral("id"), id); + QVERIFY(write(saved, document)); + QString how; + dump(saved, false, &how); + QCOMPARE(how, QStringLiteral("Project database filled from the folios: " + "two elements on a folio number their terminals alike")); } // A file whose items carry no saved uuid is filled from the folios,