Merge pull request #1105 from ispyisail/feat/stable-symbol-uuid

Give symbols in older projects the same uuid every time they open
This commit is contained in:
Laurent Trinques
2026-09-28 16:42:25 +02:00
committed by GitHub
6 changed files with 288 additions and 0 deletions
+17
View File
@@ -1707,6 +1707,23 @@ bool Diagram::fromXml(QDomElement &document,
delete nvel_elmt;
qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue");
} else {
//A symbol saved without a uuid got a random one from
//Element::fromXml(): a different identity on every load,
//written out on the next save. Derive it instead from what
//the symbol is and where it sits on its folio -- never from
//the folio's index, so inserting or moving a folio does not
//change it. Only for a folio being loaded: a paste renews
//uuids anyway.
if (consider_informations && m_project
&& QUuid(element_xml.attribute(QStringLiteral("uuid"))).isNull()) {
nvel_elmt->setUuid(m_project->derivedItemUuid(
QStringLiteral("element"),
QStringList{type_id,
element_xml.attribute(QStringLiteral("x")),
element_xml.attribute(QStringLiteral("y")),
element_xml.attribute(QStringLiteral("orientation"))}
.join(QLatin1Char('\n'))));
}
ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml));
added_elements << nvel_elmt;
}
+1
View File
@@ -245,6 +245,7 @@ class Element : public QetGraphicsItem
QString linkTypeToString() const;
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this element
void setUuid(const QUuid &uuid) {m_uuid = uuid;}
protected:
void drawAxes(QPainter *, const QStyleOptionGraphicsItem *);
+48
View File
@@ -275,6 +275,42 @@ QUuid QETProject::uuid() const
return m_uuid;
}
/**
@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
or its folio's index: inserting or moving a folio must not change it.
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::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;
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;
}
/**
@brief QETProject::init
*/
@@ -1888,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);
+4
View File
@@ -36,6 +36,7 @@
#endif
#include <QHash>
#include <QSet>
#include <QFuture>
class Diagram;
@@ -107,6 +108,7 @@ class QETProject : public QObject
ProjectPropertiesHandler& projectPropertiesHandler();
projectDataBase *dataBase();
QUuid uuid() const;
QUuid derivedItemUuid(const QString &kind, const QString &key);
ProjectState state() const;
QList<Diagram *> diagrams() const;
int folioIndex(const Diagram *) const;
@@ -365,6 +367,8 @@ class QETProject : public QObject
QFuture<bool> m_backup_future;
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;
+12
View File
@@ -329,9 +329,21 @@ target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test)
target_compile_definitions(tst_conductorselfretrace PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# A symbol saved without a uuid gets the same one on every load, and
# inserting a folio does not change it. Runs the real binary's --resave on
# fixtures/qet_bug_repro_resaved.qet with the symbols' uuids stripped.
add_executable(
tst_derivedsymboluuid
tst_derivedsymboluuid.cpp)
add_test(NAME tst_derivedsymboluuid COMMAND tst_derivedsymboluuid)
add_dependencies(tst_derivedsymboluuid qelectrotech)
target_link_libraries(tst_derivedsymboluuid PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_derivedsymboluuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Saving a project that was just saved changes nothing: runs the real
# binary's --resave twice on every project in examples/, and on one whose
# title block holds a single-space value (#973).
add_executable(
tst_resaveunchanged
tst_resaveunchanged.cpp)
+206
View File
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QDomDocument>
#include <QFile>
#include <QHash>
#include <QProcess>
#include <QProcessEnvironment>
#include <QSet>
#include <QTemporaryDir>
#include <QUuid>
// A symbol saved without a uuid must get the same one on every load of the
// same file, and inserting a folio in front of it must not change it. Until
// this was fixed it got a random uuid each time (Element::fromXml), which
// the next save wrote out.
//
// Runs the real binary (--resave) on the fixture with its symbols' uuids
// stripped, and reads the uuids back from the saved file.
namespace {
QDomDocument load(const QString &path)
{
QDomDocument doc;
QFile file(path);
if (file.open(QIODevice::ReadOnly))
doc.setContent(&file);
return doc;
}
QList<QDomElement> symbols(const QDomElement &diagram)
{
QList<QDomElement> out;
const QDomNodeList nodes = diagram.firstChildElement(QStringLiteral("elements"))
.elementsByTagName(QStringLiteral("element"));
for (int i = 0; i < nodes.size(); ++i) {
const QDomElement e = nodes.at(i).toElement();
if (e.parentNode().parentNode() == diagram)
out << e;
}
return out;
}
QList<QDomElement> diagrams(const QDomDocument &doc)
{
QList<QDomElement> out;
for (QDomElement d = doc.documentElement().firstChildElement(QStringLiteral("diagram"));
!d.isNull(); d = d.nextSiblingElement(QStringLiteral("diagram")))
out << d;
return out;
}
// Symbols without uuids, and no wires (they refer to symbols by uuid).
void stripUuids(QDomDocument &doc)
{
for (QDomElement d : diagrams(doc)) {
for (QDomElement e : symbols(d))
e.removeAttribute(QStringLiteral("uuid"));
d.removeChild(d.firstChildElement(QStringLiteral("conductors")));
}
}
// type|x|y|orientation -> uuid, for every symbol in the file
QMultiHash<QString, QString> symbolUuids(const QDomDocument &doc)
{
QMultiHash<QString, QString> out;
for (const QDomElement &d : diagrams(doc))
for (const QDomElement &e : symbols(d))
out.insert(QStringList{e.attribute(QStringLiteral("type")),
e.attribute(QStringLiteral("x")),
e.attribute(QStringLiteral("y")),
e.attribute(QStringLiteral("orientation"))}
.join(QLatin1Char('|')),
e.attribute(QStringLiteral("uuid")));
return out;
}
} // namespace
class tst_derivedsymboluuid : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
// Save @p doc, run --resave on it in a sandbox of its own (so a running
// QElectroTech cannot answer instead), and return what was saved.
QDomDocument resave(const QDomDocument &doc)
{
const QString in = m_dir.filePath(QStringLiteral("in%1.qet").arg(m_run));
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
QDir().mkpath(home);
QFile f(in);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(doc.toByteArray());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--resave"), in, out});
if (!proc.waitForFinished(60000) || proc.exitCode() != 0) return {};
return load(out);
}
QDomDocument fixture()
{
QDomDocument doc = load(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
stripUuids(doc);
return doc;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QVERIFY(!fixture().isNull());
}
void sameUuidsOnEveryLoad()
{
const QMultiHash<QString, QString> a = symbolUuids(resave(fixture()));
const QMultiHash<QString, QString> b = symbolUuids(resave(fixture()));
QCOMPARE(a.size(), 9); // the fixture's placed symbols
QCOMPARE(a, b);
for (const QString &u : a)
QVERIFY2(!QUuid(u).isNull(), qPrintable(u));
QCOMPARE(QSet<QString>(a.begin(), a.end()).size(), a.size());
}
void insertingAFolioChangesNothing()
{
QDomDocument moved = fixture();
QDomElement first = diagrams(moved).first();
QDomElement blank = first.cloneNode(false).toElement();
blank.setAttribute(QStringLiteral("title"), QStringLiteral("new"));
blank.appendChild(moved.createElement(QStringLiteral("elements")));
moved.documentElement().insertBefore(blank, first);
const QMultiHash<QString, QString> after = symbolUuids(resave(moved));
QCOMPARE(after.size(), 9);
QCOMPARE(after, symbolUuids(resave(fixture())));
}
void savedUuidsAreKept()
{
QDomDocument doc = load(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
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();
QDomElement one = symbols(diagrams(doc).first()).first();
QDomElement copy = one.cloneNode(true).toElement();
//A copy's terminals carry their own file ids; a symbol whose
//terminal ids are already taken is not loaded at all.
const QDomNodeList terminals = copy.elementsByTagName(QStringLiteral("terminal"));
for (int i = 0; i < terminals.size(); ++i)
terminals.at(i).toElement().setAttribute(QStringLiteral("id"), 90000 + i);
one.parentNode().appendChild(copy);
const QStringList both = symbolUuids(resave(doc)).values(
QStringList{one.attribute(QStringLiteral("type")),
one.attribute(QStringLiteral("x")),
one.attribute(QStringLiteral("y")),
one.attribute(QStringLiteral("orientation"))}
.join(QLatin1Char('|')));
QCOMPARE(both.size(), 2);
QVERIFY(both.at(0) != both.at(1));
}
};
QTEST_APPLESS_MAIN(tst_derivedsymboluuid)
#include "tst_derivedsymboluuid.moc"