Files
qelectrotech-source-mirror/tests/qttest/tst_terminaluuids.cpp
T
ispyisail 079d085a87 Keep terminal uuids when a whole category is replaced; never duplicate one
Review of the previous commit:
- keep() could give an old uuid to a new terminal while another terminal
  of the new definition already carried it (a moved terminal), leaving
  two terminals with one uuid. A terminal carrying any old uuid is now
  left alone, and an old uuid already in use is never handed out.
- copyDirectory() replaced a whole category of the embedded collection
  (drag a folder onto the project's folder of the same name) without
  carrying terminal uuids over. keepInDirectory() walks both trees by
  name and calls keep() on each symbol.
- The "wire(s) not loaded" log line repeated the folio's list on every
  paste; it is now written only when a folio is loaded.

tst_terminaluuids: 3 new cases, each red on the previous keep().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:11:15 +13:00

249 lines
9.0 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include "../../sources/ElementsCollection/terminaluuids.h"
#include <QtTest>
#include <QDomDocument>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <QUuid>
// Replacing a symbol's definition in a project must not cost the wires on
// it. A wire is saved against the uuids of the terminals it joins and is
// reattached on load to a terminal with that uuid, or not at all; the
// replacement is another copy of the symbol, whose terminals usually carry
// other uuids. TerminalUuids::keep() carries the old uuids over.
namespace {
QDomElement symbol(QDomDocument &doc, const QList<QStringList> &terminals)
{
QDomElement element = doc.createElement(QStringLiteral("element"));
QDomElement definition = doc.createElement(QStringLiteral("definition"));
QDomElement description = doc.createElement(QStringLiteral("description"));
element.appendChild(definition);
definition.appendChild(description);
for (const QStringList &t : terminals) {
QDomElement terminal = doc.createElement(QStringLiteral("terminal"));
terminal.setAttribute(QStringLiteral("x"), t.at(0));
terminal.setAttribute(QStringLiteral("y"), t.at(1));
terminal.setAttribute(QStringLiteral("orientation"), t.at(2));
if (t.size() > 3)
terminal.setAttribute(QStringLiteral("uuid"), t.at(3));
description.appendChild(terminal);
}
return element;
}
QStringList uuids(const QDomElement &element)
{
QStringList out;
const QDomNodeList nodes = element.elementsByTagName(QStringLiteral("terminal"));
for (int i = 0; i < nodes.size(); ++i)
out << nodes.at(i).toElement().attribute(QStringLiteral("uuid"));
return out;
}
QList<QDomElement> embeddedSymbols(const QDomDocument &doc)
{
QList<QDomElement> out;
const QDomNodeList nodes = doc.documentElement()
.firstChildElement(QStringLiteral("collection"))
.elementsByTagName(QStringLiteral("element"));
for (int i = 0; i < nodes.size(); ++i)
out << nodes.at(i).toElement();
return out;
}
}
class tst_terminaluuids : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
// --info on @p project in a sandbox of its own; returns the number of
// wires loaded and puts what was written on stderr in @p log.
int loadedWires(const QString &project, QString *log)
{
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("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("--info"), project});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return -1;
*log = QString::fromUtf8(proc.readAllStandardError());
const QByteArray out = proc.readAllStandardOutput();
const QJsonDocument json = QJsonDocument::fromJson(out.mid(out.indexOf('{')));
return json.object().value(QStringLiteral("conductors")).toInt(-1);
}
// 2612_ats_singlephase.qet with each embedded symbol replaced by a copy
// whose terminals carry new uuids, passed through keep() or not.
QString replacedSymbols(bool keep_uuids)
{
QFile in(QStringLiteral(QET_EXAMPLES_DIR "/2612_ats_singlephase.qet"));
if (!in.open(QIODevice::ReadOnly)) return {};
QDomDocument doc;
if (!doc.setContent(&in)) return {};
for (const QDomElement &old_symbol : embeddedSymbols(doc)) {
QDomElement new_symbol = old_symbol.cloneNode().toElement();
const QDomNodeList terminals = new_symbol.elementsByTagName(QStringLiteral("terminal"));
for (int i = 0; i < terminals.size(); ++i)
terminals.at(i).toElement().setAttribute(QStringLiteral("uuid"),
QUuid::createUuid().toString());
if (keep_uuids)
TerminalUuids::keep(old_symbol, new_symbol);
old_symbol.parentNode().replaceChild(new_symbol, old_symbol);
}
const QString path = m_dir.filePath(keep_uuids ? QStringLiteral("kept.qet")
: QStringLiteral("lost.qet"));
QFile out(path);
if (!out.open(QIODevice::WriteOnly)) return {};
out.write(doc.toByteArray());
return path;
}
private slots:
void samePlaceTakesOldUuid()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"},
{"20", "10", "s", "{b}"}});
QDomElement new_symbol = symbol(doc, {{"20.0", "10", "s", "{y}"},
{"0", "10.0", "n", "{x}"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{b}", "{a}"}));
}
void movedOrNewTerminalKeepsItsOwn()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"}});
QDomElement new_symbol = symbol(doc, {{"0", "10", "e", "{x}"},
{"0", "20", "n", "{y}"},
{"0", "30", "n"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{x}", "{y}", ""}));
}
void oldWithoutUuidChangesNothing()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n"}});
QDomElement new_symbol = symbol(doc, {{"0", "10", "n", "{x}"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{x}"}));
}
void twoAtOnePlaceMatchedInOrder()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"},
{"0", "10", "n", "{b}"}});
QDomElement new_symbol = symbol(doc, {{"0", "10", "n", "{x}"},
{"0", "10", "n", "{y}"},
{"0", "10", "n", "{z}"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{b}", "{z}"}));
}
// The new revision moved terminal {a} and put a new one where it was:
// {a} is still {a}, and the new one is not given {a} a second time.
void noUuidGivenTwice()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "0", "n", "{a}"}});
QDomElement new_symbol = symbol(doc, {{"0", "5", "n", "{a}"},
{"0", "0", "n", "{c}"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{c}"}));
}
// A terminal that already carries an old uuid is never renamed, even
// when it moved onto the place of another old terminal.
void movedTerminalNotRenamed()
{
QDomDocument doc;
const QDomElement old_symbol = symbol(doc, {{"0", "0", "n", "{a}"},
{"0", "5", "n", "{b}"}});
QDomElement new_symbol = symbol(doc, {{"0", "5", "n", "{a}"},
{"0", "9", "n", "{x}"}});
TerminalUuids::keep(old_symbol, new_symbol);
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{x}"}));
}
// Replacing a whole category: every symbol with a counterpart of the
// same name, at any depth, keeps its terminal uuids.
void keepInDirectory()
{
QDomDocument doc;
auto category = [&doc](const QString &name) {
QDomElement c = doc.createElement(QStringLiteral("category"));
c.setAttribute(QStringLiteral("name"), name);
return c;
};
auto named = [](QDomElement e, const QString &name) {
e.setAttribute(QStringLiteral("name"), name);
return e;
};
QDomElement old_dir = category(QStringLiteral("dir"));
QDomElement old_sub = category(QStringLiteral("sub"));
old_dir.appendChild(named(symbol(doc, {{"0", "0", "n", "{a}"}}), "top.elmt"));
old_dir.appendChild(old_sub);
old_sub.appendChild(named(symbol(doc, {{"0", "0", "n", "{b}"}}), "deep.elmt"));
QDomElement new_dir = category(QStringLiteral("dir"));
QDomElement new_sub = category(QStringLiteral("sub"));
const QDomElement top = named(symbol(doc, {{"0", "0", "n", "{x}"}}), "top.elmt");
const QDomElement other = named(symbol(doc, {{"0", "0", "n", "{y}"}}), "other.elmt");
const QDomElement deep = named(symbol(doc, {{"0", "0", "n", "{z}"}}), "deep.elmt");
new_dir.appendChild(top);
new_dir.appendChild(other);
new_dir.appendChild(new_sub);
new_sub.appendChild(deep);
TerminalUuids::keepInDirectory(old_dir, new_dir);
QCOMPARE(uuids(top), QStringList{"{a}"});
QCOMPARE(uuids(other), QStringList{"{y}"});
QCOMPARE(uuids(deep), QStringList{"{b}"});
}
// The real loader, on a project whose 131 wires are saved against
// terminal uuids: replaced symbols lose 119 of them unless keep() ran,
// and the ones lost are reported.
void wiresSurviveReplacedSymbols()
{
QVERIFY(m_dir.isValid());
QString log;
const QString lost = replacedSymbols(false);
QVERIFY(!lost.isEmpty());
QCOMPARE(loadedWires(lost, &log), 12);
QVERIFY2(log.contains(QStringLiteral("55 wire(s) not loaded"))
&& log.contains(QStringLiteral("64 wire(s) not loaded")),
"the lost wires were not reported");
const QString kept = replacedSymbols(true);
QVERIFY(!kept.isEmpty());
QCOMPARE(loadedWires(kept, &log), 131);
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
}
};
QTEST_APPLESS_MAIN(tst_terminaluuids)
#include "tst_terminaluuids.moc"