Files
qelectrotech-source-mirror/tests/qttest/tst_derivedsymboluuid.cpp
T
ispyisail 0721b42e21 Give symbols saved without a uuid the same one on every load
A symbol saved without a uuid got a random one from Element::fromXml()
on every load, and the next save wrote it out: two loads of the same file
gave the same symbol two identities, and anything pointing at it by uuid
(a script, a comparison of two versions, a wire's identity) could not
follow it from one session to the next.

When a folio is loaded, such a symbol now gets a UUID v5 derived from
what it is and where it sits: its type, its position on the folio and its
orientation. Never the folio's index, so inserting or moving a folio does
not change it. Identical symbols stacked on one spot, or a copied folio,
are told apart by a counter kept per project (QETProject::derivedUuid()),
in load order among those symbols alone. A paste still renews uuids.

Symbols that have a uuid in the file keep it. All 24 example projects
already have one for every symbol, so they are unchanged; with the
symbols' uuids stripped, each saves byte-for-byte the same twice (master:
different every time).

tst_derivedsymboluuid runs --resave on a fixture with its uuids stripped:
same uuids on every load, same after a folio is inserted in front, saved
uuids kept, stacked copies differ. The first two fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:13:02 +13:00

185 lines
5.8 KiB
C++

// 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));
}
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"