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"