Merge master into fix/joined-wire-uuid

The conflict was tests/qttest/CMakeLists.txt, where #1520 added
tst_savedstamps at the place this branch adds tst_joinedwireuuid. Both
are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-18 20:50:24 +13:00
13 changed files with 545 additions and 113 deletions
+93 -51
View File
@@ -847,13 +847,15 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
}
}
query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) "
"VALUES (:element_uuid, :linked_uuid, :group_index)"));
query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) "
"VALUES (:element_uuid, :linked_uuid, :group_index, :ord)"));
for (const DocumentElement &element : std::as_const(elements)) {
int ord = 0;
for (const auto &link : element.links) {
query.bindValue(QStringLiteral(":element_uuid"), element.uuid);
query.bindValue(QStringLiteral(":linked_uuid"), link.first);
query.bindValue(QStringLiteral(":group_index"), link.second >= 0 ? QVariant(link.second) : QVariant());
query.bindValue(QStringLiteral(":ord"), ord++);
if (!query.exec()) {
qDebug() << "projectDataBase::populateFromDocument link insert error : " << query.lastError();
}
@@ -1092,7 +1094,7 @@ QList<QUuid> projectDataBase::linkedSymbols(const QUuid &element) const
{
QList<QUuid> linked;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY rowid"));
query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY ord, rowid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec()) {
while (query.next())
@@ -2602,12 +2604,14 @@ bool projectDataBase::createDataBase()
//Create the link table: one row per element and element it is
//linked to -- a coil and its contacts, a pair of folio reports --
//from each side, with the contact group the element saved for it.
//from each side, with the contact group the element saved for it,
//and its place (ord) in the order the element saves its links.
const QString link_table(QStringLiteral(
"CREATE TABLE link ("
"element_uuid VARCHAR(50) NOT NULL, "
"linked_uuid VARCHAR(50) NOT NULL, "
"group_index INTEGER, "
"ord INTEGER NOT NULL DEFAULT 0, "
"PRIMARY KEY (element_uuid, linked_uuid), "
"FOREIGN KEY (element_uuid) REFERENCES element (uuid))"));
if (!query_.exec(link_table)) {
@@ -3868,10 +3872,11 @@ QString projectDataBase::prefillReport() const
void projectDataBase::populateLinkTable()
{
m_dirty_link_elements.clear();
const QSet<QUuid> waiting = waitingSymbols();
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("DELETE FROM link"));
query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) "
"VALUES (:element_uuid, :linked_uuid, :group_index)"));
query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) "
"VALUES (:element_uuid, :linked_uuid, :group_index, :ord)"));
for (const auto &diagram : m_project->folios())
{
@@ -3885,23 +3890,7 @@ void projectDataBase::populateLinkTable()
}
const ElementProvider ep(diagram);
for (const auto &elmt : ep.find(allElementTypes()))
{
QList<std::pair<QString, int>> rows;
for (Element *linked : elmt->linkedElements())
rows.append({linked->uuid().toString(), elmt->groupIndexForElement(linked)});
//Partners on a folio not built, which it will be linked to
for (const auto &waiting : elmt->waitingLinks())
rows.append({waiting.first.toString(), waiting.second});
for (const auto &row : std::as_const(rows))
{
query.bindValue(QStringLiteral(":element_uuid"), elmt->uuid().toString());
query.bindValue(QStringLiteral(":linked_uuid"), row.first);
query.bindValue(QStringLiteral(":group_index"), row.second >= 0 ? QVariant(row.second) : QVariant());
if (!query.exec()) {
qDebug() << "projectDataBase::populateLinkTable insert error : " << query.lastError();
}
}
}
insertLinkRows(query, elmt, waiting);
}
}
@@ -3927,6 +3916,83 @@ void projectDataBase::queueLinks(Element *element)
}
}
/**
@brief projectDataBase::linkRowMismatches
A test and debugging aid (QET_CHECK_LINK_ROWS): the placed
symbols whose link rows are not the links they save, in order
(Element::linksToSave(), insertLinkRows())
@return one entry per such symbol: its uuid
*/
QStringList projectDataBase::linkRowMismatches()
{
flushLinks();
const QSet<QUuid> waiting = waitingSymbols();
QStringList mismatches;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link "
"WHERE element_uuid = :uuid ORDER BY ord, rowid"));
for (auto it = m_placed_elements.cbegin() ; it != m_placed_elements.cend() ; ++it)
{
Element *element = it.value();
if (!element) {
continue;
}
QList<std::pair<QString, int>> expected;
for (const Element::SavedLink &link : element->linksToSave(waiting))
if (!link.built || m_placed_elements.contains(link.uuid, link.built))
expected.append({link.uuid.toString(), link.group_index});
QList<std::pair<QString, int>> rows;
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
if (query.exec())
while (query.next())
rows.append({query.value(0).toString(),
query.value(1).isNull() ? -1 : query.value(1).toInt()});
if (rows != expected) {
mismatches << element->uuid().toString();
}
}
return mismatches;
}
/**
@brief projectDataBase::waitingSymbols
@return the symbols on folios not built yet (lazy folios): partners a
symbol keeps its saved links to until they are built
*/
QSet<QUuid> projectDataBase::waitingSymbols() const
{
return m_project && m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios()
: QSet<QUuid>();
}
/**
@brief projectDataBase::insertLinkRows
Write @p element's link rows with @p insert (prepared with :element_uuid,
:linked_uuid, :group_index and :ord): its partners as it saves them
(Element::linksToSave()), in that order, so the rows and the file say
the same: @p waiting is the symbols on folios not built, as a save
takes them (waitingSymbols()). A built partner the project no longer has (on a removed
folio, linked still for the undo) gets no row.
*/
void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element,
const QSet<QUuid> &waiting)
{
int ord = 0;
for (const Element::SavedLink &link : element->linksToSave(waiting))
{
if (link.built && !m_placed_elements.contains(link.uuid, link.built)) {
continue;
}
insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString());
insert.bindValue(QStringLiteral(":linked_uuid"), link.uuid.toString());
insert.bindValue(QStringLiteral(":group_index"), link.group_index >= 0 ? QVariant(link.group_index) : QVariant());
insert.bindValue(QStringLiteral(":ord"), ord++);
if (!insert.exec()) {
qDebug() << "projectDataBase::insertLinkRows insert error : " << insert.lastError();
}
}
}
/**
@brief projectDataBase::flushLinks
Write the link rows of every element queued by linksChanged()
@@ -3939,12 +4005,13 @@ void projectDataBase::flushLinks()
const auto dirty = m_dirty_link_elements;
m_dirty_link_elements.clear();
const QSet<QUuid> waiting = waitingSymbols();
const bool own_transaction = m_data_base.transaction();
QSqlQuery remove(m_data_base);
remove.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid"));
QSqlQuery insert(m_data_base);
insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) "
"VALUES (:element_uuid, :linked_uuid, :group_index)"));
insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) "
"VALUES (:element_uuid, :linked_uuid, :group_index, :ord)"));
for (const QPointer<Element> &element : dirty)
{
if (!element) {
@@ -3960,32 +4027,7 @@ void projectDataBase::flushLinks()
if (!m_placed_elements.contains(element->uuid(), element)) {
continue;
}
for (Element *linked : element->linkedElements())
{
//A symbol on a removed folio stays linked, for the undo,
//but the project no longer has it
if (!m_placed_elements.contains(linked->uuid(), linked)) {
continue;
}
const int group = element->groupIndexForElement(linked);
insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString());
insert.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString());
insert.bindValue(QStringLiteral(":group_index"), group >= 0 ? QVariant(group) : QVariant());
if (!insert.exec()) {
qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError();
}
}
//Partners on a folio not built (lazy folios), which it will
//be linked to once that folio is
for (const auto &waiting : element->waitingLinks())
{
insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString());
insert.bindValue(QStringLiteral(":linked_uuid"), waiting.first.toString());
insert.bindValue(QStringLiteral(":group_index"), waiting.second >= 0 ? QVariant(waiting.second) : QVariant());
if (!insert.exec()) {
qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError();
}
}
insertLinkRows(insert, element, waiting);
}
if (own_transaction) {
m_data_base.commit();
+3
View File
@@ -154,6 +154,7 @@ class projectDataBase : public QObject
void conductorPropertiesStored(Conductor *conductor, const ConductorProperties &properties);
void conductorUuidChanged(Conductor *conductor, const QUuid &old_uuid);
QStringList conductorPropertiesMismatches() const;
QStringList linkRowMismatches();
//Fill the three stores from a project's document before its
//folios are built, then, once
@@ -253,6 +254,8 @@ class projectDataBase : public QObject
void populateConductorTable();
void populateLinkTable();
void flushLinks();
void insertLinkRows(QSqlQuery &insert, Element *element, const QSet<QUuid> &waiting);
QSet<QUuid> waitingSymbols() const;
void populateDrawingItemTables();
bool populateFromDocument(const QDomDocument &document, QString *why = nullptr);
bool writeDrawingItem(QObject *object);
+17
View File
@@ -2342,6 +2342,23 @@ bool Diagram::fromXml(QDomElement &document,
<< "wire end(s) on a terminal that moved since the project was saved,"
<< "matched to it by place";
}
//A copy of a symbol sharing its uuid with another (older versions'
//copies, F100) gets its own, now that its wires are connected to
//it: wires and links then name one symbol each
//(QETProject::renewedSharedUuid(), only while the project opens)
if (consider_informations && m_project) {
const int folio_index = m_project->folioIndex(this);
QHash<QUuid, int> ordinal;
for (Element *element : std::as_const(added_elements)) {
const QUuid shared = element->uuid();
const QUuid renewed = m_project->renewedSharedUuid(folio_index, ordinal[shared]++,
shared, uuid());
if (!renewed.isNull()) {
element->setUuid(renewed);
}
}
}
if (consider_informations && !m_wires_not_reconnected.isEmpty()) {
qWarning().noquote() << "Diagram::fromXml():"
<< m_wires_not_reconnected.size()
+43 -25
View File
@@ -1437,32 +1437,11 @@ QDomElement Element::toXml(
element.appendChild(inputs);
//if this element is linked to other elements,
//save the uuid of each other elements
//A partner on a folio not built yet (lazy folios), which this
//symbol will be linked to, is saved too, in the order the symbol was
//saved with: saving needs no partner built (stage D5a)
//save the uuid of each other elements (linksToSave())
QList<std::pair<QUuid, int>> links;
if (keepsSavedLinks() && diagram() && diagram()->project()) {
const QSet<QUuid> waiting = diagram()->project()->waitingLinkTargets();
QSet<Element *> written;
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
Element *linked = nullptr;
for (Element *elmt : std::as_const(connected_elements))
if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; }
if (linked) {
written.insert(linked);
links.append({linked->uuid(), m_group_index_map.value(linked, -1)});
} else if (waiting.contains(link_info.uuid)) {
links.append({link_info.uuid, link_info.group_index});
}
}
for (Element *elmt : std::as_const(connected_elements))
if (!written.contains(elmt))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)});
} else {
for (Element *elmt : std::as_const(connected_elements))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)});
}
for (const SavedLink &link : linksToSave(diagram() && diagram()->project()
? diagram()->project()->waitingLinkTargets() : QSet<QUuid>()))
links.append({link.uuid, link.group_index});
if (!links.isEmpty()) {
QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids"));
for (const auto &link : std::as_const(links)) {
@@ -1869,6 +1848,45 @@ void Element::initLink(QETProject *prj)
}
}
/**
@brief Element::linksToSave
The partners this symbol saves, in the order it saves them: the order
it was saved with (tmp_uuids_link), then partners linked since. A
partner on a folio not built yet (lazy folios), which this symbol will
be linked to, is in the list too: saving needs no partner built (stage
D5a). toXml() writes this list, and the project database's link rows
are written from it, so the two are the same (QETZ-DB-FIRST stage 2a).
@param waiting the symbols on folios not built: a saved partner among
them is kept (QETProject::symbolsOnUnbuiltFolios())
@return each partner's uuid, its contact group (-1 if none), and the
partner itself when it is built (nullptr for one waiting)
*/
QList<Element::SavedLink> Element::linksToSave(const QSet<QUuid> &waiting) const
{
QList<SavedLink> links;
if (keepsSavedLinks() && diagram() && diagram()->project()) {
QSet<Element *> written;
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
Element *linked = nullptr;
for (Element *elmt : std::as_const(connected_elements))
if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; }
if (linked) {
written.insert(linked);
links.append({linked->uuid(), m_group_index_map.value(linked, -1), linked});
} else if (waiting.contains(link_info.uuid)) {
links.append({link_info.uuid, link_info.group_index, nullptr});
}
}
for (Element *elmt : std::as_const(connected_elements))
if (!written.contains(elmt))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt});
} else {
for (Element *elmt : std::as_const(connected_elements))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt});
}
return links;
}
/**
@brief Element::pendingLinks
@return the uuids of the partners this symbol was saved linked to and
+7
View File
@@ -227,6 +227,13 @@ class Element : public QetGraphicsItem
virtual void unlinkElement(Element *) {}
virtual void initLink(QETProject *);
QSet<QUuid> pendingLinks() const;
/// A partner as the symbol saves it (linksToSave())
struct SavedLink {
QUuid uuid;
int group_index = -1;
Element *built = nullptr; ///< nullptr: on a folio not built yet
};
QList<SavedLink> linksToSave(const QSet<QUuid> &waiting) const;
bool keepsSavedLinks() const {return !tmp_uuids_link.isEmpty();}
QList<std::pair<QUuid, int>> waitingLinks() const;
/**
+118 -24
View File
@@ -299,6 +299,27 @@ QByteArray QETProject::containerPicture(const QString &file) const
return m_container_pictures.value(file);
}
/**
@brief QETProject::renewedSharedUuid
While the project opens: the uuid of the @p ordinal -th symbol carrying
@p uuid on the folio at @p folio_index (in the file's order), when
other symbols of the project carry the same uuid and this copy is not
the one keeping it (readDiagramsXml()). Derived from the uuid and the
folio's uuid, so the same file gives the same uuid on every open.
@return a null uuid when this symbol keeps its uuid
*/
QUuid QETProject::renewedSharedUuid(int folio_index, int ordinal, const QUuid &uuid,
const QUuid &folio_uuid)
{
const auto keeper = m_shared_uuid_keepers.constFind(uuid);
if (keeper == m_shared_uuid_keepers.constEnd()
|| (keeper->first == folio_index && keeper->second == ordinal)) {
return QUuid();
}
return derivedItemUuid(QStringLiteral("element-copy"),
uuid.toString() + QLatin1Char('\n') + folio_uuid.toString());
}
/**
@brief QETProject::derivedItemUuid
A uuid for an item of this project that was saved without one, the same
@@ -474,6 +495,8 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
}
QFileInfo fi(*file);
setFilePath(fi.absoluteFilePath());
//Saved again unchanged, the file keeps its saved date and path
m_stamped_path = m_file_path;
//Extract the content of the xml. The raw bytes are kept : a project
//file without a persisted uuid derives its uuid from them.
@@ -1161,16 +1184,8 @@ void QETProject::setFilePath(const QString &filepath)
setReadOnly(false);
}
//title block variables should be updated after file save as dialog is confirmed, before file is saved.
m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat));
m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy"));
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm"));
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName());
m_project_properties.addValue("savedfilepath", filePath());
//The saved date, time and path of the title block are set by
//write(), when the project is saved (stampSaved())
emit projectFilePathChanged(this, m_file_path);
emit projectInformationsChanged(this);
updateDiagramsFolioData();
@@ -2636,6 +2651,16 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
// Write the elements collection.
project_root.appendChild(m_elements_collection->root().cloneNode(true));
//QET_CHECK_LINK_ROWS=1: say whether the project database's link rows
//are the links each placed symbol saves, in its order, once the
//folios are written. Builds nothing (lazy folios stay as they are):
//a test and debugging aid; it changes nothing.
if (qEnvironmentVariableIntValue("QET_CHECK_LINK_ROWS") == 1) {
const QStringList links = m_data_base.linkRowMismatches();
qWarning().noquote() << "link rows:" << links.size() << "differ"
<< links.mid(0, 5).join(QStringLiteral(", "));
}
m_waiting_link_targets.clear();
return(xml_doc);
}
@@ -2801,6 +2826,33 @@ bool QETProject::changedOnDisk(QDateTime *when) const
return true;
}
/**
@brief QETProject::stampSaved
Set the title block variables of the save (saveddate, saveddate-eu,
saveddate-us, savedtime, savedfilename, savedfilepath) to now and to
the file being written, when there is something new to save: the
project changed since it was opened or last saved, or it is written to
another file. Otherwise they keep what the file holds, so saving an
unchanged project gives an identical file (F124).
@return true if they were set
*/
bool QETProject::stampSaved()
{
const bool changed = m_modified || m_undo_stack->index() != m_stamped_undo_index;
if (!changed && m_file_path == m_stamped_path) {
return false;
}
m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat));
m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy"));
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm"));
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName());
m_project_properties.addValue("savedfilepath", filePath());
emit projectInformationsChanged(this);
updateDiagramsFolioData();
return true;
}
/**
@brief QETProject::write
Save the project in a file.
@@ -2868,13 +2920,28 @@ QETResult QETProject::write(bool overwrite_changed)
if (!PreviousVersions::keep(m_file_path, PreviousVersions::count(), &backup_error))
qWarning().noquote() << "Previous version of" << m_file_path << "not kept:" << backup_error;
//The saved date, time and path go into the file being written, and
//only when there is something new to save: an unchanged project
//saves to an identical file (F124, a project kept in git). A
//failed write puts the previous values back.
const DiagramContext stamped_before = m_project_properties;
const bool stamped = stampSaved();
const auto failed_stamped = [&](const QString &error) {
if (stamped) {
m_project_properties = stamped_before;
emit projectInformationsChanged(this);
updateDiagramsFolioData();
}
return failed(error);
};
QString error_message;
if (isContainerPath(m_file_path))
{
//A container is written from the whole document: every folio
//built (stage D4 writes it from the database)
if (!writeContainerFile(m_file_path, &error_message))
return(failed(error_message));
return(failed_stamped(error_message));
}
else
{
@@ -2884,7 +2951,7 @@ QETResult QETProject::write(bool overwrite_changed)
QDomDocument xml_project(toXml(&kept_folios));
if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
m_file_path, &error_message))
return(failed(error_message));
return(failed_stamped(error_message));
}
rememberDiskState();
@@ -2905,17 +2972,8 @@ QETResult QETProject::write(bool overwrite_changed)
}
}
//title block variables should be updated after file save dialog is confirmed, before file is saved.
m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat));
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy"));
m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm"));
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName());
m_project_properties.addValue("savedfilepath", filePath());
emit projectInformationsChanged(this);
updateDiagramsFolioData();
m_stamped_path = m_file_path;
m_stamped_undo_index = m_undo_stack->index();
setModified(false);
return(QETResult());
}
@@ -3637,6 +3695,41 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
}
}
//Symbols sharing a uuid (copies made by older versions, F100): the
//project database, links and wires name a symbol by its uuid, so
//each copy but one gets its own (renewedSharedUuid()). The copy
//that keeps it is the first one linked to another symbol (a
//partner's link names the uuid), else the first. The folios with
//a copy to renew are built now, also with lazy folios, so the
//whole project has one uuid per symbol from the start.
m_shared_uuid_keepers.clear();
QSet<int> build_now;
{
QHash<QUuid, QList<std::tuple<int, int, bool>>> copies;
for (int i = 0 ; i < diagram_nodes.length() ; ++i) {
const QDomElement elements = diagram_nodes.at(i).toElement()
.firstChildElement(QStringLiteral("elements"));
QHash<QUuid, int> ordinal;
for (QDomElement e = elements.firstChildElement(QStringLiteral("element"));
!e.isNull() ; e = e.nextSiblingElement(QStringLiteral("element"))) {
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
if (uuid.isNull()) continue;
const bool linked = !e.firstChildElement(QStringLiteral("links_uuids"))
.firstChildElement(QStringLiteral("link_uuid")).isNull();
copies[uuid].append({i, ordinal[uuid]++, linked});
}
}
for (auto it = copies.cbegin() ; it != copies.cend() ; ++it) {
if (it.value().size() < 2) continue;
auto keeper = it.value().first();
for (const auto &copy : it.value())
if (std::get<2>(copy)) { keeper = copy; break; }
m_shared_uuid_keepers.insert(it.key(), {std::get<0>(keeper), std::get<1>(keeper)});
for (const auto &copy : it.value())
if (copy != keeper) build_now.insert(std::get<0>(copy));
}
}
if(dlgWaiting)
dlgWaiting->setProgressBarRange(0, diagram_nodes.length()*3);
@@ -3690,7 +3783,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
this, &QETProject::usedTitleBlockTemplateChanged);
m_deferring_folios = m_lazy_open;
m_deferring_folios = m_lazy_open && !build_now.contains(i);
diagram->initFromXml(diagram_xml_element);
m_deferring_folios = false;
if (!diagram->isLoaded() && !line_starts.isEmpty())
@@ -3703,6 +3796,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
}
}
m_shared_uuid_keepers.clear();
updateDiagramsFolioData();
//Initialise links between elements in this project
+12 -1
View File
@@ -123,6 +123,8 @@ class QETProject : public QObject
projectDataBase *dataBase();
QUuid uuid() const;
QUuid derivedItemUuid(const QString &kind, const QString &key);
QUuid renewedSharedUuid(int folio_index, int ordinal, const QUuid &uuid,
const QUuid &folio_uuid);
QByteArray containerPicture(const QString &file) const;
ProjectState state() const;
QString openError() const;
@@ -441,6 +443,7 @@ class QETProject : public QObject
}
private:
bool stampSaved();
void readProjectXml(QDomDocument &xml_project);
void readDiagramsXml(QDomDocument &xml_project);
void readElementsCollectionXml(QDomDocument &xml_project);
@@ -570,6 +573,10 @@ class QETProject : public QObject
bool m_kept_folios_trusted = false;
/// Read from a .qet whose path nobody chose since: saved beside it
bool m_opened_from_qet = false;
/// The file the saved date and path were set for, and the undo
/// index then: an unchanged project keeps them (stampSaved())
QString m_stamped_path;
int m_stamped_undo_index = 0;
/// The project file as this QET last read or wrote it, to see
/// whether something else changed it since (changedOnDisk())
QString m_disk_path;
@@ -610,7 +617,11 @@ class QETProject : public QObject
int m_next_backup_slot = 0;
QUuid m_uuid = QUuid::createUuid();
QHash<QString, int> m_derived_uuid_keys;
QSet<QUuid> m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid()
QSet<QUuid> m_saved_item_uuids;
/// While opening: per uuid carried by several symbols, the copy
/// keeping it (folio index, ordinal on that folio), see
/// renewedSharedUuid()
QHash<QUuid, std::pair<int, int>> m_shared_uuid_keepers; //symbol and wire uuids the file carries, see derivedItemUuid()
projectDataBase m_data_base;
QVector<TerminalStrip *> m_terminal_strip_vector;
QVector<Cable *> m_cables;
+12
View File
@@ -889,6 +889,18 @@ if(QET_HAS_SCRIPTING)
target_compile_definitions(tst_scriptconductoruuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The saved date, time and path of the title block change only when the
# project changed, and go into the file that save writes (F124): an
# unchanged project saved twice gives an identical file.
add_executable(
tst_savedstamps
tst_savedstamps.cpp)
add_test(NAME tst_savedstamps COMMAND tst_savedstamps)
add_dependencies(tst_savedstamps qelectrotech)
target_link_libraries(tst_savedstamps PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_savedstamps PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Deleting a symbol whose terminal held several wires joins them with a
# new wire; its uuid comes from its two ends, so the same deletion saves
# the same file every time (F115): examples/affuteuse_250h.qet.
+3 -1
View File
@@ -214,7 +214,9 @@ class tst_projectlock : public QObject
void readOnlyFolderDoesNotBlock()
{
#ifndef Q_OS_WIN
#ifdef Q_OS_WIN
QSKIP("a read-only folder still takes new files on Windows");
#else
if (geteuid() == 0)
QSKIP("root can write anywhere");
#endif
+6 -3
View File
@@ -69,10 +69,12 @@ private slots:
f.close();
// Columns numbered from 1, as QElectroTech's settings page
// shows by default
// shows by default. Written to an INI file QET_SETTINGS_DIR
// points at: on Windows the settings are in the registry
// otherwise, and the columns would count from 0
const QString home = m_dir.filePath(tag + QStringLiteral("-home"));
QVERIFY(QDir().mkpath(home + QStringLiteral("/config/QElectroTech")));
QFile conf(home + QStringLiteral("/config/QElectroTech/QElectroTech.conf"));
QVERIFY(QDir().mkpath(home + QStringLiteral("/settings/QElectroTech")));
QFile conf(home + QStringLiteral("/settings/QElectroTech/QElectroTech.ini"));
QVERIFY(conf.open(QIODevice::WriteOnly));
conf.write("[General]\nborder-columns_0=false\n");
conf.close();
@@ -80,6 +82,7 @@ private slots:
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("QET_SETTINGS_DIR"), home + QStringLiteral("/settings"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
+28 -2
View File
@@ -411,14 +411,15 @@ private slots:
// QElectroTech run in a sandbox of its own with @p args, the store
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output
QString runChecked(const QStringList &args, bool eager = false)
QString runChecked(const QStringList &args, bool eager = false,
const char *check = "QET_CHECK_ELEMENT_INFO_STORE")
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
QDir().mkpath(home);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
env.insert(QString::fromLatin1(check), QStringLiteral("1"));
if (eager) {
env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening
}
@@ -452,6 +453,31 @@ private slots:
QVERIFY2(out.contains(QStringLiteral("wire properties store: 0 differ")), qPrintable(out.right(600)));
}
// The link rows say what each symbol saves, in its order, also after
// an edit builds a partner's folio (lazy folios): m_000 as a .qetz
// saved with every folio built, a symbol of its middle folio moved. The rows used to follow the order
// the partners were built in (QETZ-DB-FIRST stage 2a).
void linkRowsAfterLazyEdit()
{
const QString qetz = m_dir.filePath(QStringLiteral("links%1.qetz").arg(m_run));
QVERIFY(!runChecked({QStringLiteral("--resave"),
QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"), qetz}, true).isEmpty());
const QString saved = m_dir.filePath(QStringLiteral("links-moved%1.qetz").arg(m_run));
const QString js = m_dir.filePath(QStringLiteral("links%1.js").arg(m_run));
QFile script(js);
QVERIFY(script.open(QIODevice::WriteOnly));
script.write(QStringLiteral(
"var f = Math.floor(qet.folioCount() / 2);\n"
"var u = qet.elementUuids(f).slice().sort()[0];\n"
"qet.log('MOVED ' + qet.moveElement(f, u, 0, 100));\n"
"qet.save('%1');\n").arg(saved).toUtf8());
script.close();
const QString out = runChecked({QStringLiteral("--run"), js, qetz}, false,
"QET_CHECK_LINK_ROWS");
QVERIFY2(out.contains(QStringLiteral("MOVED true")), qPrintable(out.right(600)));
QVERIFY2(out.contains(QStringLiteral("link rows: 0 differ")), qPrintable(out.right(600)));
}
// The stores filled from the document before the folios are built
// hold what building them gives, for a
// project saved by this version (every symbol, folio and wire carries
+143
View File
@@ -0,0 +1,143 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QDate>
#include <QDomDocument>
#include <QProcess>
#include <QTemporaryDir>
/**
The title block variables of a save (saveddate, savedtime,
savedfilename, savedfilepath...) are set when the project is saved
with something new in it, and go into the file that save writes. They
used to be set after writing, so each save wrote the time of the one
before: a project saved twice without a change gave two different
files, and a changed project was saved with the previous date.
Runs the real binary on fixtures/unlinked_contact_label.qet, whose
stamps say 2026-10-02 18:39 in a temporary folder:
- saved twice in its own file without a change: byte-identical;
- one symbol moved, then saved: today's date and the file's own path.
*/
class tst_savedstamps : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QString m_base;
QString run(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
//Its own settings and single-instance socket: another test's
//QElectroTech running at the same time would otherwise be
//handed this run (SingleApplication)
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.setProcessChannelMode(QProcess::MergedChannels);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
if (!proc.waitForFinished(120000) || proc.exitStatus() != QProcess::NormalExit
|| proc.exitCode() != 0)
return QString();
return QString::fromUtf8(proc.readAll()) + QStringLiteral("\nEXIT 0");
}
QString script(const QString &name, const QString &body)
{
const QString path = m_dir.filePath(name);
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) return QString();
file.write(body.toUtf8());
return path;
}
/// The saved-* properties of @p qetz, read back through --resave
QMap<QString, QString> stamps(const QString &qetz)
{
const QString xml = qetz + QStringLiteral(".qet");
if (run({QStringLiteral("--resave"), qetz, xml}).isEmpty()) return {};
QFile file(xml);
QDomDocument doc;
if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return {};
QMap<QString, QString> out;
const QDomNodeList props = doc.documentElement().firstChildElement(
QStringLiteral("properties")).elementsByTagName(QStringLiteral("property"));
for (int i = 0; i < props.size(); ++i) {
const QDomElement p = props.at(i).toElement();
if (p.attribute(QStringLiteral("name")).startsWith(QLatin1String("saved")))
out.insert(p.attribute(QStringLiteral("name")), p.text());
}
return out;
}
static QByteArray bytes(const QString &path)
{
QFile file(path);
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
}
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
QVERIFY(m_dir.isValid());
const QString fixture = QFINDTESTDATA("fixtures/unlinked_contact_label.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
//The fixture as a .qetz, its stamps as they were: a copy
//written by --resave is not a save of the project
m_base = m_dir.filePath(QStringLiteral("base.qetz"));
QVERIFY(!run({QStringLiteral("--resave"), fixture, m_base}).isEmpty());
QCOMPARE(stamps(m_base).value(QStringLiteral("saveddate-us")), QStringLiteral("2026-10-02"));
}
void unchangedSavesAreIdentical()
{
const QString project = m_dir.filePath(QStringLiteral("unchanged.qetz"));
QVERIFY(QFile::copy(m_base, project));
const QString js = script(QStringLiteral("unchanged.js"),
QStringLiteral("qet.save('');\nqet.save('');\nqet.log('SAVED twice');\n"));
QVERIFY(run({QStringLiteral("--run"), js, project}).contains(QLatin1String("SAVED twice")));
QCOMPARE(bytes(project), bytes(m_base));
}
void changedSaveIsStampedNow()
{
const QString project = m_dir.filePath(QStringLiteral("changed.qetz"));
QVERIFY(QFile::copy(m_base, project));
const QString js = script(QStringLiteral("changed.js"), QStringLiteral(
"var u = qet.elementUuids(0)[0];\n"
"qet.log('MOVED ' + qet.moveElement(0, u, 10, 0));\n"
"qet.save('');\n"));
QVERIFY(run({QStringLiteral("--run"), js, project}).contains(QLatin1String("MOVED true")));
const QMap<QString, QString> s = stamps(project);
QCOMPARE(s.value(QStringLiteral("saveddate-us")), QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd")));
QCOMPARE(s.value(QStringLiteral("savedfilepath")), QFileInfo(project).absoluteFilePath());
QCOMPARE(s.value(QStringLiteral("savedfilename")), QStringLiteral("changed"));
}
};
QTEST_GUILESS_MAIN(tst_savedstamps)
#include "tst_savedstamps.moc"
+60 -6
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QSet>
#include <QDomDocument>
#include <QFile>
@@ -150,13 +151,66 @@ private slots:
QVERIFY(!twice.isEmpty());
QCOMPARE(nets(twice), expected);
//No wire names a lamp by the uuid they share
for (const QDomElement &c : childElements(firstDiagram(load(twice)),
QStringLiteral("conductors"),
QStringLiteral("conductor"))) {
QVERIFY(c.attribute(QStringLiteral("element1")) != shared);
QVERIFY(c.attribute(QStringLiteral("element2")) != shared);
//Each lamp has a uuid of its own after a save: one keeps the
//shared uuid, the others get one derived from it
QSet<QString> uuids;
for (const QDomElement &e : lamps(load(twice)))
uuids.insert(e.attribute(QStringLiteral("uuid")));
QCOMPARE(uuids.size(), 3);
QVERIFY(uuids.contains(shared));
}
// A linked contact and an unlinked symbol carrying the same uuid (a copy
// made by an older version): the linked one keeps the uuid, the other
// gets its own, and every link and wire stays where it was.
void linkedCopyKeepsUuid()
{
const QString source = QStringLiteral(QET_EXAMPLES_DIR "/affuteuse_250h.qet");
QDomDocument doc = load(source);
QList<QDomElement> all;
const QDomNodeList folios = doc.documentElement().elementsByTagName(QStringLiteral("diagram"));
for (int i = 0; i < folios.size(); ++i)
all << childElements(folios.at(i).toElement(), QStringLiteral("elements"),
QStringLiteral("element"));
auto linked = [](const QDomElement &e) {
return !e.firstChildElement(QStringLiteral("links_uuids"))
.firstChildElement(QStringLiteral("link_uuid")).isNull();
};
QDomElement keeper, copy;
for (const QDomElement &e : std::as_const(all)) {
if (keeper.isNull() && linked(e)) keeper = e;
else if (copy.isNull() && !linked(e)) copy = e;
}
QVERIFY(!keeper.isNull() && !copy.isNull());
const QString shared = keeper.attribute(QStringLiteral("uuid"));
copy.setAttribute(QStringLiteral("uuid"), shared);
const int wires = doc.elementsByTagName(QStringLiteral("conductor")).size();
const QString saved = resave(write(doc));
QVERIFY(!saved.isEmpty());
const QDomDocument out = load(saved);
QHash<QString, QDomElement> by_uuid;
QList<std::pair<QString, QString>> links;
const QDomNodeList saved_folios = out.documentElement().elementsByTagName(QStringLiteral("diagram"));
for (int i = 0; i < saved_folios.size(); ++i)
for (const QDomElement &e : childElements(saved_folios.at(i).toElement(),
QStringLiteral("elements"),
QStringLiteral("element"))) {
const QString u = e.attribute(QStringLiteral("uuid"));
QVERIFY2(!by_uuid.contains(u), qPrintable(QStringLiteral("uuid still shared: ") + u));
by_uuid.insert(u, e);
for (QDomElement l = e.firstChildElement(QStringLiteral("links_uuids"))
.firstChildElement(QStringLiteral("link_uuid"));
!l.isNull(); l = l.nextSiblingElement(QStringLiteral("link_uuid")))
links.append({u, l.attribute(QStringLiteral("uuid"))});
}
QVERIFY(by_uuid.contains(shared));
QVERIFY(linked(by_uuid.value(shared)));
for (const auto &link : std::as_const(links)) {
QVERIFY2(by_uuid.contains(link.second), "a link names a symbol not in the project");
QVERIFY2(links.contains(std::make_pair(link.second, link.first)), "a link only one side has");
}
QCOMPARE(out.elementsByTagName(QStringLiteral("conductor")).size(), wires);
}
void savedByUuidReopensRight()