Never derive a uuid the file already carries

A symbol keeps its derived uuid once saved, even when it is moved. A
symbol saved without a uuid that later turns up on the spot it left -- a
hand edit, an older version, another tool writing the file -- derived the
same uuid, and the project had two symbols with one identity.

readDiagramsXml() now collects every symbol and wire uuid the file
carries, on any folio, before a folio loads; derivedItemUuid() moves to
the next counter value while a candidate is among them. The result still
depends on the file alone. Renamed from derivedUuid(), which QETProject
already has for the project's own uuid.

tst_derivedsymboluuid: newcomerOnAMovedSymbolsSpotGetsAnotherUuid fails
with the check switched off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-28 20:46:01 +13:00
parent 0721b42e21
commit e12410e439
4 changed files with 58 additions and 7 deletions
+1 -1
View File
@@ -1685,7 +1685,7 @@ bool Diagram::fromXml(QDomElement &document,
//uuids anyway.
if (consider_informations && m_project
&& QUuid(element_xml.attribute(QStringLiteral("uuid"))).isNull()) {
nvel_elmt->setUuid(m_project->derivedUuid(
nvel_elmt->setUuid(m_project->derivedItemUuid(
QStringLiteral("element"),
QStringList{type_id,
element_xml.attribute(QStringLiteral("x")),
+32 -5
View File
@@ -276,7 +276,7 @@ QUuid QETProject::uuid() const
}
/**
@brief QETProject::derivedUuid
@brief QETProject::derivedItemUuid
A uuid for an item of this project that was saved without one, the same
on every load of the same file.
@p key describes the item by what it is, never by its place in the file
@@ -284,16 +284,31 @@ QUuid QETProject::uuid() const
Items with the same @p kind and @p key anywhere in the project (a copied
folio, two identical symbols stacked on one spot) are told apart by a
counter, in load order among those items alone.
A derived uuid is never one the file already carries: an item saved with
a derived uuid and then moved or re-connected keeps it, so a newcomer
later taking its old place or ends would otherwise derive the same one.
The file's saved uuids are collected before any folio loads
(readDiagramsXml()), so the result still depends on the file alone.
uuids are unique within one project; copies of a project share them, as
they share every saved uuid. Anything bringing items in from another
project must renew them, as paste does.
@return a UUID v5, which cannot collide with the v4 uuids given to new
items
*/
QUuid QETProject::derivedUuid(const QString &kind, const QString &key)
QUuid QETProject::derivedItemUuid(const QString &kind, const QString &key)
{
static const QUuid derived_ns(QStringLiteral("{7d1e9c3a-5b2f-4e8a-9c61-2f4b8d0e6a17}"));
const QString full = kind + QLatin1Char('\n') + key;
const int n = m_derived_uuid_keys[full]++;
return QUuid::createUuidV5(derived_ns,
n ? full + QLatin1Char('\n') + QString::number(n) : full);
int &n = m_derived_uuid_keys[full];
QUuid uuid;
do {
uuid = QUuid::createUuidV5(derived_ns,
n ? full + QLatin1Char('\n') + QString::number(n) : full);
++n;
} while (m_saved_item_uuids.contains(uuid));
return uuid;
}
/**
@@ -1909,6 +1924,18 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
//Search the diagrams in the project
QDomNodeList diagram_nodes = xml_project.elementsByTagName(QStringLiteral("diagram"));
//Every symbol and wire uuid the file already carries, on any folio,
//before a folio derives one for an item saved without: see
//derivedItemUuid().
for (const QString &tag : {QStringLiteral("element"), QStringLiteral("conductor")}) {
const QDomNodeList nodes = xml_project.elementsByTagName(tag);
for (int i = 0; i < nodes.size(); ++i) {
const QUuid saved(nodes.at(i).toElement().attribute(QStringLiteral("uuid")));
if (!saved.isNull())
m_saved_item_uuids.insert(saved);
}
}
if(dlgWaiting)
dlgWaiting->setProgressBarRange(0, diagram_nodes.length()*3);
+3 -1
View File
@@ -36,6 +36,7 @@
#endif
#include <QHash>
#include <QSet>
#include <QFuture>
class Diagram;
@@ -107,7 +108,7 @@ class QETProject : public QObject
ProjectPropertiesHandler& projectPropertiesHandler();
projectDataBase *dataBase();
QUuid uuid() const;
QUuid derivedUuid(const QString &kind, const QString &key);
QUuid derivedItemUuid(const QString &kind, const QString &key);
ProjectState state() const;
QList<Diagram *> diagrams() const;
int folioIndex(const Diagram *) const;
@@ -367,6 +368,7 @@ class QETProject : public QObject
KAutoSaveFile m_backup_file;
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()
projectDataBase m_data_base;
QVector<TerminalStrip *> m_terminal_strip_vector;
+22
View File
@@ -156,6 +156,28 @@ private slots:
QCOMPARE(symbolUuids(resave(doc)), symbolUuids(doc));
}
// A symbol keeps its derived uuid once saved, even when moved. A symbol
// saved without a uuid that later turns up on the spot it left (a hand
// edit, an older version, another tool) would derive the same uuid: it
// must get another one instead.
void newcomerOnAMovedSymbolsSpotGetsAnotherUuid()
{
QDomDocument doc = resave(fixture());
QDomElement moved = symbols(diagrams(doc).first()).first();
QVERIFY(!QUuid(moved.attribute(QStringLiteral("uuid"))).isNull());
QDomElement newcomer = moved.cloneNode(true).toElement();
newcomer.removeAttribute(QStringLiteral("uuid"));
const QDomNodeList terminals = newcomer.elementsByTagName(QStringLiteral("terminal"));
for (int i = 0; i < terminals.size(); ++i)
terminals.at(i).toElement().setAttribute(QStringLiteral("id"), 90000 + i);
moved.setAttribute(QStringLiteral("x"), moved.attribute(QStringLiteral("x")).toInt() + 500);
moved.parentNode().appendChild(newcomer);
const QMultiHash<QString, QString> after = symbolUuids(resave(doc));
QCOMPARE(after.size(), 10);
QCOMPARE(QSet<QString>(after.begin(), after.end()).size(), 10);
}
void stackedIdenticalSymbolsDiffer()
{
QDomDocument doc = fixture();