From 798010cc833c846e9627869f46e064060864ffb5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 12 Oct 2026 23:26:39 +1300 Subject: [PATCH] Change a wire's properties by uuid without building its folios (stage D5) Scripts get conductorPropertyByUuid() and setConductorPropertyByUuid(), the wire named by its uuid (conductorUuids()) rather than by a symbol's terminal. A change goes to every wire at the wire's potential, as setConductorProperty() does. When those wires are all on folios not built, none is numbered by a formula, and what changes in each is what a .qetz database holds in its own columns (number, function, voltage, colour and section of the wire, cable, bus), it goes through the project database: the store and the conductor rows, undoable by uuid. The potential is followed in the database (projectDataBase::potentialWires(): through wire ends, folio reports and terminal blocks, as Conductor::relatedPotentialConductors() follows it; a terminal block that isolates its terminals answers nothing). Building a wire takes its change (Conductor::fromXml()); a .qetz save writes it in the database, the folio file copied; a folio written from its XML gets it there; a .qet builds. Anything else builds the folio, as before. tst_lazyfolios: every wire's potential from the database = as followed, 1 342 of 1 342 (industrial, eager and from a .qetz); a cable set by uuid on a 14-wire potential builds no folio, reaches every wire, saves as when built, undoes, shows once built (fails when building ignores the change, and when the .qetz writer does). The test's run limit is raised (500 s -> 1 500 s, ctest 1 800 s): under load it ran past 500 s. ctest 96/96 both ways. Co-Authored-By: Claude Opus 5.5 --- sources/container/qetcontainerdb.cpp | 17 +++ sources/dataBase/projectdatabase.cpp | 173 +++++++++++++++++++++++++ sources/dataBase/projectdatabase.h | 9 ++ sources/qetgraphicsitem/conductor.cpp | 5 + sources/qetproject.cpp | 21 +++ sources/scripting/qetscriptapi.cpp | 178 ++++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 10 ++ tests/qttest/CMakeLists.txt | 2 +- tests/qttest/lazy_folios_probe.cpp | 92 +++++++++++++ tests/qttest/run_lazy_folios_probe.py | 2 +- 10 files changed, 507 insertions(+), 2 deletions(-) diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index 340a22b47..85349e9fc 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -818,6 +818,23 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB ? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(), value, attributesJson(i, QStringLiteral("name"), true)}); } + } + //Its wires whose properties changed since: their columns + for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-conductor"))) { + QStringList set; + QVariantList values; + for (const QString &column : ConductorData) { + if (column == QLatin1String("uuid") || column.startsWith(QLatin1String("element")) + || column.startsWith(QLatin1String("terminal")) || column == QLatin1String("freezeLabel") + || !edited.hasAttribute(column)) + continue; + set << QStringLiteral("\"%1\" = ?").arg(column); + values << edited.attribute(column); + } + values << id << edited.attribute(QStringLiteral("uuid")); + Statement update(db, QStringLiteral("UPDATE conductor SET %1 WHERE folio_id = ? AND uuid = ?") + .arg(set.join(QStringLiteral(", ")))); + ok = ok && !set.isEmpty() && update.run(values) && sqlite3_changes(db.m_db) == 1; } if (!ok) { setError(error, QStringLiteral("folio %1 kept as it was is not in its database").arg(id)); diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index c7c84c4a8..32085c9b3 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -1317,6 +1317,77 @@ QString projectDataBase::terminalName(const QUuid &terminal, const QUuid &elemen return query.exec() && query.next() ? query.value(0).toString() : QString(); } +/** + @brief projectDataBase::potentialWires + @return the wires at the potential of @p wire, @p wire included, read + from the conductor, element and link tables as they are (see + elementTypesOnFolios()), followed as Conductor::relatedPotentialConductors() + follows them over every folio: through a wire's ends, from a folio + report to the terminals of the first symbol it is linked to, and from a + terminal block's terminal to its others. Nothing when a terminal block + on the way isolates its terminals (their groups are in its definition, + not in the database) or @p wire has no row. +*/ +std::optional> projectDataBase::potentialWires(const QUuid &wire) const +{ + using Node = std::pair; //element uuid, terminal uuid + QHash> ends; + QHash wires_at; + QHash> nodes_of; + { + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("SELECT uuid, terminal1_element_uuid, terminal1_uuid, " + "terminal2_element_uuid, terminal2_uuid FROM conductor")); + while (query.next()) { + const Node a{query.value(1).toString(), query.value(2).toString()}; + const Node b{query.value(3).toString(), query.value(4).toString()}; + const QString uuid = query.value(0).toString(); + ends.insert(uuid, {a, b}); + for (const Node &n : {a, b}) { + wires_at[n] << uuid; + if (!nodes_of.value(n.first).contains(n)) nodes_of[n.first] << n; + } + } + } + const auto start = ends.constFind(wire.toString()); + if (start == ends.constEnd()) { + return std::nullopt; + } + QHash kinds; + { + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("SELECT uuid, type FROM element")); + while (query.next()) + kinds.insert(query.value(0).toString(), ElementData::typeFromString(query.value(1).toString())); + } + QSet wires; + QSet seen; + QList todo{start->first, start->second}; + while (!todo.isEmpty()) { + const Node node = todo.takeLast(); + if (seen.contains(node)) continue; + seen.insert(node); + for (const QString &w : wires_at.value(node)) { + if (wires.contains(QUuid(w))) continue; + wires.insert(QUuid(w)); + const auto e = ends.value(w); + todo << e.first << e.second; + } + const ElementData::Type kind = kinds.value(node.first, ElementData::Simple); + if (kind & ElementData::AllReport) { + const QList partners = linkedSymbols(QUuid(node.first)); + if (!partners.isEmpty()) + todo << nodes_of.value(partners.first().toString()); + } else if (kind & ElementData::Terminal) { + if (QET::infoFlagIsTrue(m_element_information.value(QUuid(node.first)) + .value(QStringLiteral("potential_isolating")).toString())) + return std::nullopt; + todo << nodes_of.value(node.first); + } + } + return wires; +} + /** @brief projectDataBase::linkedSymbols @return the symbols @p element is linked to, read from the link table @@ -1467,9 +1538,111 @@ QSet projectDataBase::foliosWithUnbuiltEdits() const { QSet folios; for (const auto &edit : m_unbuilt_edits) folios.insert(edit.folio); + for (const auto &edit : m_unbuilt_wire_edits) folios.insert(edit.folio); return folios; } +/** + @brief projectDataBase::unbuiltWire + @return true if the wire @p wire is on a folio not built (lazy folios): + it has a row and its properties are stored, and no wire built holds its + uuid. @p folio is set to its folio. +*/ +bool projectDataBase::unbuiltWire(const QUuid &wire, QUuid *folio) const +{ + if (placedConductorCount(wire) != 0 || !hasConductorProperties(wire)) { + return false; + } + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT diagram_uuid FROM conductor WHERE uuid = :uuid")); + query.bindValue(QStringLiteral(":uuid"), wire.toString()); + if (!query.exec() || !query.next()) { + return false; + } + if (folio) *folio = QUuid(query.value(0).toString()); + return !query.next(); +} + +/** + @brief projectDataBase::editUnbuiltConductorProperties + Give the wire @p wire, on a folio not built (unbuiltWire()), the + properties @p properties as Conductor::setProperties() would: the store, + its rows and its conductor row's text. Building it takes them + (takeUnbuiltWireEdit()); a .qetz saves them in its database (QETProject:: + toXml()), anything else builds its folio to save. +*/ +void projectDataBase::editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties) +{ + QUuid folio; + if (!unbuiltWire(wire, &folio)) { + return; + } + m_unbuilt_wire_edits.insert(wire, {properties, folio}); + if (!storeConductorProperties(wire, properties)) { + return; + } + m_content_changed = true; + if (!m_update_blocked) { + m_update_conductor_query.bindValue(QStringLiteral(":uuid"), wire.toString()); + m_update_conductor_query.bindValue(QStringLiteral(":text"), properties.text); + if (!m_update_conductor_query.exec()) { + qDebug() << "projectDataBase::editUnbuiltConductorProperties update error : " << m_update_conductor_query.lastError(); + } + } + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::takeUnbuiltWireEdit + @return the properties @p wire was given while its folio was not built, + forgotten here: the wire being built takes them +*/ +std::optional projectDataBase::takeUnbuiltWireEdit(const QUuid &wire) +{ + const auto edit = m_unbuilt_wire_edits.constFind(wire); + if (edit == m_unbuilt_wire_edits.constEnd()) { + return std::nullopt; + } + const ConductorProperties properties = edit->properties; + m_unbuilt_wire_edits.erase(edit); + return properties; +} + +/** + @brief projectDataBase::unbuiltWireEditsOnFolio + @return the properties given to the wires of the folio @p folio while it + was not built, by wire +*/ +QHash projectDataBase::unbuiltWireEditsOnFolio(const QUuid &folio) const +{ + QHash edits; + for (auto it = m_unbuilt_wire_edits.cbegin() ; it != m_unbuilt_wire_edits.cend() ; ++it) + if (it->folio == folio) edits.insert(it.key(), it->properties); + return edits; +} + +/** + @brief projectDataBase::setPlacedConductorProperties + Give the wire @p wire its properties @p properties: through the wire when + it is built, through the store when it is not (unbuiltWire()). What + undoing and redoing a change of wires not built does, the wires having + perhaps been built since. + @return false if neither can +*/ +bool projectDataBase::setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties) +{ + const QList> placed = m_placed_conductors.values(wire); + if (placed.size() == 1 && placed.first()) { + placed.first()->setProperties(properties); + return true; + } + if (placed.isEmpty() && unbuiltWire(wire)) { + editUnbuiltConductorProperties(wire, properties); + return true; + } + return false; +} + /** @brief projectDataBase::unbuiltEditsOnFolio @return the information changed through the store of the symbols on the diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 8d95fe0c5..ceaeec512 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -186,6 +186,11 @@ class projectDataBase : public QObject QSet foliosWithUnbuiltEdits() const; QHash unbuiltEditsOnFolio(const QUuid &folio, bool *need_build = nullptr) const; bool setPlacedElementInformation(const QUuid &element, const DiagramContext &information); + bool unbuiltWire(const QUuid &wire, QUuid *folio = nullptr) const; + void editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties); + std::optional takeUnbuiltWireEdit(const QUuid &wire); + QHash unbuiltWireEditsOnFolio(const QUuid &folio) const; + bool setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties); QHash elementTypesOnFolios(const QSet &folios) const; /// A symbol placed on a folio, as the element table has it struct FolioSymbol { @@ -203,6 +208,7 @@ class projectDataBase : public QObject QList wiresOnFolios(const QList &folios) const; QString elementLabel(const QUuid &element) const; QString terminalName(const QUuid &terminal, const QUuid &element) const; + std::optional> potentialWires(const QUuid &wire) const; static QVariant sequenceValue(const autonum::sequentialNumbers &sequence); static autonum::sequentialNumbers sequenceFromValue(const QVariant &value); @@ -346,6 +352,9 @@ class projectDataBase : public QObject /// each is on: building the symbol takes it (takeUnbuiltEdit()) struct UnbuiltEdit { DiagramContext information; QUuid folio; bool had_information; }; QHash m_unbuilt_edits; + /// The same for wires (editUnbuiltConductorProperties()) + struct UnbuiltWireEdit { ConductorProperties properties; QUuid folio; }; + QHash m_unbuilt_wire_edits; QHash m_folio_titleblocks; QHash> m_placed_folios; QHash m_conductor_properties; diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 339c4a243..f6d3b8ae4 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1154,6 +1154,11 @@ bool Conductor::fromXml(QDomElement &dom_element) m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false; + //Changed through the project database while its folio was not + //built (lazy folios): the change, not what the file says + if (diagram() && diagram()->project()) + if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltWireEdit(m_uuid)) + pr = *edited; setProperties(pr); return retval; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 1bf67e8a9..d03f15633 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -2512,6 +2512,17 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList const QDomElement old_information = element.firstChildElement(QStringLiteral("elementInformations")); element.replaceChild(information, old_information); //it has one (unbuiltEditsOnFolio()) } + //Its wires whose properties changed + const QHash wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid()); + if (!wire_edits.isEmpty()) + for (const QDomElement &list : QET::findInDomElement(xml_diagram, QStringLiteral("conductors"))) + for (QDomElement wire = list.firstChildElement(QStringLiteral("conductor")) ; !wire.isNull() ; + wire = wire.nextSiblingElement(QStringLiteral("conductor"))) { + const auto edit = wire_edits.constFind(QUuid(wire.attribute(QStringLiteral("uuid")))); + if (edit == wire_edits.constEnd()) continue; + for (const auto &attribute : edit->attributes()) + wire.setAttribute(attribute.first, attribute.second); + } xml_diagram.setAttribute("order", order_num ++); project_root.appendChild(xml_diagram); continue; @@ -2530,6 +2541,16 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList it.value().toXml(information, QStringLiteral("elementInformation")); symbol.appendChild(information); placeholder.appendChild(symbol); + } + //Its wires whose properties changed: what the .qetz database + //holds of them (only that can change, QetScriptApi) + const QHash wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid()); + for (auto it = wire_edits.cbegin() ; it != wire_edits.cend() ; ++it) { + auto wire = xml_doc.createElement(QStringLiteral("qetz-edited-conductor")); + wire.setAttribute(QStringLiteral("uuid"), it.key().toString()); + for (const auto &attribute : it.value().attributes()) + wire.setAttribute(attribute.first, attribute.second); + placeholder.appendChild(wire); } kept_parts->append(diagram->deferredPart()); project_root.appendChild(placeholder); diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 3f372bdfb..e8b7f70e5 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -1265,6 +1265,16 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu .arg(caller, value, property)); return false; } + return applyConductorProperties(conductor, properties); +} + +/** + @brief QetScriptApi::applyConductorProperties + Give @p conductor and every wire at its potential the properties + @p properties, one undo step +*/ +bool QetScriptApi::applyConductorProperties(Conductor *conductor, const ConductorProperties &properties) +{ if (properties == conductor->properties()) return true; // already so QSet potential = conductor->relatedPotentialConductors(true); @@ -1282,6 +1292,174 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu return true; } +namespace { + //The wire properties a .qetz's database holds in its own columns, which + //nothing on a folio computes from but a wire's number formula +const QStringList &storedConductorPropertyNames() +{ + static const QStringList names { + QStringLiteral("num"), QStringLiteral("function"), QStringLiteral("bus"), + QStringLiteral("cable"), QStringLiteral("tension_protocol"), + QStringLiteral("conductor_color"), QStringLiteral("conductor_section")}; + return names; +} + + //true if @p a and @p b differ only in storedConductorPropertyNames() +bool differOnlyInStored(const ConductorProperties &a, const ConductorProperties &b) +{ + QHash left, right; + for (const auto &attribute : a.attributes()) left.insert(attribute.first, attribute.second); + for (const auto &attribute : b.attributes()) right.insert(attribute.first, attribute.second); + QSet names(left.keyBegin(), left.keyEnd()); + for (const QString &name : right.keys()) names.insert(name); + for (const QString &name : std::as_const(names)) + if (left.value(name) != right.value(name) && !storedConductorPropertyNames().contains(name)) + return false; + return true; +} + + //Wires given their properties by uuid, built or not (lazy folios): + //projectDataBase::setPlacedConductorProperties() +class ChangeStoredConductorPropertiesCommand : public QUndoCommand +{ + public: + ChangeStoredConductorPropertiesCommand(QETProject *project, + const QHash> &changes) : + m_project(project), m_changes(changes) + { setText(QObject::tr("Edit the conductor properties")); } + void undo() override { apply(false); } + void redo() override { apply(true); } + private: + void apply(bool forward) { + if (!m_project) return; + for (auto it = m_changes.cbegin() ; it != m_changes.cend() ; ++it) + m_project->dataBase()->setPlacedConductorProperties( + it.key(), forward ? it->second : it->first); + } + QPointer m_project; + QHash> m_changes; +}; +} + +/** + @brief QetScriptApi::findConductorByUuid + @return the wire @p conductorUuid of the folio @p folioIndex, the folio + built, or nullptr (said in the log) +*/ +Conductor *QetScriptApi::findConductorByUuid(int folioIndex, const QString &conductorUuid, + const QString &caller) +{ + if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) { + log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); + return nullptr; + } + const QUuid wanted(conductorUuid); + for (Conductor *c : built(m_project->folios().at(folioIndex))->conductors()) + if (c->uuid() == wanted) return c; + log(QStringLiteral("qet.%1: no conductor %2 on folio %3").arg(caller, conductorUuid).arg(folioIndex)); + return nullptr; +} + +/** + @brief QetScriptApi::unbuiltConductorChange + Change the property @p property of the wire @p wire, on the folio + @p folioIndex not built (lazy folios), and of every wire at its potential, + through the project database, as applyConductorProperties() would: when + they are all on folios not built, none is numbered by a formula, and what + changes in each is only what the .qetz database holds + (storedConductorPropertyNames()) -- nothing then is computed from it. + @return true if done; false for the folio to be built, @p refused set when + the value itself is not one +*/ +bool QetScriptApi::unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property, + const QString &value, bool *refused) +{ + *refused = false; + projectDataBase *database = m_project->dataBase(); + Diagram *folio = m_project->folios().at(folioIndex); + QUuid on; + if (folio->isLoaded() || !database->rowsFromDocument() || !storedConductorPropertyNames().contains(property) + || !database->unbuiltWire(wire, &on) || on != folio->uuid()) { + return false; + } + ConductorProperties properties = database->conductorProperties(wire); + if (!setConductorPropertyValue(properties, property, value)) { + *refused = true; + return false; + } + if (!properties.m_formula.isEmpty()) { + return false; + } + const auto potential = database->potentialWires(wire); + if (!potential) { + return false; + } + QHash> changes; + for (const QUuid &other : *potential) { + if (!database->unbuiltWire(other)) return false; + const ConductorProperties old = database->conductorProperties(other); + if (!differOnlyInStored(old, properties)) return false; + if (!(old == properties)) changes.insert(other, {old, properties}); + } + if (!changes.isEmpty()) + m_project->undoStack()->push(new ChangeStoredConductorPropertiesCommand(m_project, changes)); + return true; +} + +QString QetScriptApi::conductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property) const +{ + if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return QString(); + Diagram *folio = m_project->folios().at(folioIndex); + projectDataBase *database = m_project->dataBase(); + QUuid on; + if (!folio->isLoaded() && database->rowsFromDocument() + && database->unbuiltWire(QUuid(conductorUuid), &on) && on == folio->uuid()) + return conductorPropertyValue(database->conductorProperties(QUuid(conductorUuid)), property); + auto *self = const_cast(this); + Conductor *conductor = self->findConductorByUuid(folioIndex, conductorUuid, + QStringLiteral("conductorPropertyByUuid")); + return conductor ? conductorPropertyValue(conductor->properties(), property) : QString(); +} + +bool QetScriptApi::setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property, const QString &value) +{ + if (!m_project) return false; + const QString caller = QStringLiteral("setConductorPropertyByUuid"); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return false; + } + if (!conductorPropertyNames().contains(property)) { + log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3") + .arg(caller, property, conductorPropertyNames().join(QStringLiteral(", ")))); + return false; + } + if (folioIndex < 0 || folioIndex >= m_project->folios().count()) { + log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); + return false; + } + //The folio left unbuilt when the change can go through the project + //database (stage D5) + bool refused = false; + if (unbuiltConductorChange(folioIndex, QUuid(conductorUuid), property, value, &refused)) + return true; + if (refused) { + log(QStringLiteral("qet.%1: '%2' is not a valid value for %3").arg(caller, value, property)); + return false; + } + Conductor *conductor = findConductorByUuid(folioIndex, conductorUuid, caller); + if (!conductor) return false; + ConductorProperties properties = conductor->properties(); + if (!setConductorPropertyValue(properties, property, value)) { + log(QStringLiteral("qet.%1: '%2' is not a valid value for %3") + .arg(caller, value, property)); + return false; + } + return applyConductorProperties(conductor, properties); +} + /** @brief QetScriptApi::conductorSegments List the drawn path of the conductor on a terminal, one line per diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 6a954d5a6..9d0c9c7b7 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -34,6 +34,7 @@ class DiagramView; class Element; class Terminal; class Conductor; +class ConductorProperties; class IndependentTextItem; class QetShapeItem; class DiagramImageItem; @@ -440,6 +441,11 @@ class QetScriptApi : public QObject Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &property, const QString &value); + // The same, the wire named by its uuid (conductorUuids()) + Q_INVOKABLE QString conductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property) const; + Q_INVOKABLE bool setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property, const QString &value); // -- a conductor's own drawn path, not the whole potential's // properties above -- one conductor only, addressed the same way -- @@ -648,6 +654,10 @@ class QetScriptApi : public QObject private: bool runFlag(const QString &flag, const QStringList &args); Diagram *built(Diagram *folio) const; + Conductor *findConductorByUuid(int folioIndex, const QString &conductorUuid, const QString &caller); + bool applyConductorProperties(Conductor *conductor, const ConductorProperties &properties); + bool unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property, + const QString &value, bool *refused); std::optional unbuiltInformation(int folioIndex, const QString &elementUuid) const; Element *findElement(int folioIndex, const QString &elementUuid) const; Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index b5be0cf96..2418a9ef0 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -92,7 +92,7 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --build ${CMAKE_BINARY_DIR} --binary $ --source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp --fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet) - set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 600 RUN_SERIAL TRUE + set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 1800 RUN_SERIAL TRUE ENVIRONMENT "QT_QPA_PLATFORM=offscreen") # Deleting a cable, undoing the delete twice and then editing anything # has both cable commands tearing down the same cable. Runs under the diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp index 2bc143b93..630dca6db 100644 --- a/tests/qttest/lazy_folios_probe.cpp +++ b/tests/qttest/lazy_folios_probe.cpp @@ -777,6 +777,28 @@ int main(int argc, char **argv) std::printf(".qetz drawn items through their view: %d lazily, %d when built\n", lazily, built); check(built > 0, "the project has drawn items"); check(lazily == built, "the drawn items' view as when built"); + } + //Every wire's potential from the database as QElectroTech + //follows it with every folio built + { + QETProject indexed_potentials(zipped); + int same = 0, differ = 0, unknown = 0; + for (Diagram *folio : eager.folios()) + for (Conductor *wire : folio->conductors()) { + QSet live{wire->uuid()}; + for (Conductor *c : wire->relatedPotentialConductors(true)) live.insert(c->uuid()); + for (QETProject *project : {&eager, &indexed_potentials}) { + const auto stored = project->dataBase()->potentialWires(wire->uuid()); + if (!stored) { ++unknown; continue; } + if (*stored == live) { ++same; continue; } + if (differ++ < 3) + std::printf(" potential of %s: %d wires followed, %d from the database\n", + qPrintable(wire->uuid().toString()), int(live.size()), int(stored->size())); + } + } + std::printf("potentials from the database: %d same, %d differ, %d not answered\n", same, differ, unknown); + check(differ == 0 && same > 0, "every wire's potential from the database as followed"); + check(indexed_potentials.unloadedFolioCount() > 0, "potentials read without building"); } //The folios' rows as saved, not as reading their headers one //by one numbers them ("1/1" for the first of three, D7) @@ -997,6 +1019,76 @@ int main(int argc, char **argv) revisionEveryFolio(zipped, output, "revised", path); revisionEveryFolio(zipped, output, "relabelled", path, busiest); + //A script changes a wire's cable by its uuid, on folios not + //built, the wire and its potential numbered by hand: through + //the project database, no folio built; saved as when built; + //undone; the folio built afterwards shows it (D5) + { + QETProject cabled(zipped); + const int unbuilt = cabled.unloadedFolioCount(); + int folio_index = -1; + QString uuid; + QSet potential; + for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) { + if (cabled.folios().at(i)->isLoaded()) continue; + for (Conductor *wire : folios.at(i)->conductors()) { + QSet wires = wire->relatedPotentialConductors(true); + wires << wire; + bool usable = true; + for (Conductor *c : std::as_const(wires)) + if (!c->properties().m_formula.isEmpty() + || cabled.folios().at(eager.folioIndex(c->diagram()))->isLoaded()) + usable = false; + if (!usable || wires.size() < 2) continue; + folio_index = i; + uuid = wire->uuid().toString(); + for (Conductor *c : std::as_const(wires)) potential.insert(c->uuid()); + break; + } + } + check(!uuid.isEmpty(), "a wire numbered by hand, its potential on folios not built"); + QetScriptApi api(&cabled); + const QString before = api.conductorPropertyByUuid(folio_index, uuid, "cable"); + const QString value = QStringLiteral("W-42"); + check(api.setConductorPropertyByUuid(folio_index, uuid, "cable", value), "a script changes a wire not built"); + check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == value, "the wire's change read back"); + for (const QUuid &wire : std::as_const(potential)) + check(cabled.dataBase()->conductorProperties(wire).m_cable == value, "every wire of the potential changed"); + check(cabled.unloadedFolioCount() == unbuilt, "changing wires not built builds no folio"); + const QString cabled_path = output + "/cabled.qetz"; + check(cabled.writeContainerFile(cabled_path, &error), "saved after the wire change"); + std::printf(".qetz wire on folio %d, potential of %d wires, cable changed by uuid: 0 folios built, %d by saving\n", + folio_index + 1, int(potential.size()), unbuilt - cabled.unloadedFolioCount()); + qputenv("QET_EAGER_FOLIOS", "1"); + const QString usual_path = output + "/cabled-usual.qetz"; + { + QETProject usual_cabled(zipped); + check(QetScriptApi(&usual_cabled).setConductorPropertyByUuid(folio_index, uuid, "cable", value), + "the same wire change when built"); + check(usual_cabled.writeContainerFile(usual_path, &error), "saved after the wire change when built"); + } + QETProject cabled_back(cabled_path), usual_back(usual_path); + qunsetenv("QET_EAGER_FOLIOS"); + auto lines = [&path](QETProject &project) { + QStringList list = sortedLines(project.toXml().toString(), path); + list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); }); + return list; + }; + check(lines(cabled_back) == lines(usual_back), "a wire changed through the database saved as when built"); + cabled.undoStack()->undo(); + check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == before, "the wire change undone"); + cabled.undoStack()->redo(); + Conductor *built_wire = nullptr; + for (Conductor *wire : cabled.folios().at(folio_index)->conductors()) + if (wire->uuid().toString() == uuid) built_wire = wire; + check(!built_wire, "the folio not built yet"); + cabled.buildFolioToShow(cabled.folios().at(folio_index)); + for (Conductor *wire : cabled.folios().at(folio_index)->conductors()) + if (wire->uuid().toString() == uuid) built_wire = wire; + check(built_wire && built_wire->properties().m_cable == value, + "a wire changed before its folio was built shows the change"); + } + //The wire numbers and wiring list exports, and the terminal //numbering dialog: as when built, building only the folios //holding a terminal (D5) diff --git a/tests/qttest/run_lazy_folios_probe.py b/tests/qttest/run_lazy_folios_probe.py index 2c7f881cb..af41a0fcc 100644 --- a/tests/qttest/run_lazy_folios_probe.py +++ b/tests/qttest/run_lazy_folios_probe.py @@ -54,7 +54,7 @@ subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], che output = build / 'lazy-folios' output.mkdir(exist_ok=True) report = output / 'results.txt' -result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=500) +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=1500) if report.exists(): print(report.read_text(encoding='utf-8', errors='replace')) sys.exit(result.returncode)