From f49abcc6cc0921d44866e3370ddc20d203bdf24b Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 09:52:41 +1300 Subject: [PATCH] Fix a deleted symbol's joining wire getting a random uuid on every run Deleting a symbol whose terminal held several wires joins the wires left behind with a new one. That wire got a random uuid, so the same deletion saved a different file every time: a diff for nothing in a project kept in git (discussion #1512), and two copies with the same edit could not be compared. Its uuid now comes from its two ends (each end's symbol and terminal, sorted), the recipe a wire saved without a uuid already gets on load (Diagram::fromXml(), derivedItemUuid()). Undo and redo keep the same wire, so its uuid too. tst_joinedwireuuid deletes the coil KA1 of examples/affuteuse_250h.qet in two runs: both saves are identical and no wire has a random uuid. Without the fix one wire has a random uuid. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR --- .../deleteqgraphicsitemcommand.cpp | 16 +++ tests/qttest/CMakeLists.txt | 13 +++ tests/qttest/tst_joinedwireuuid.cpp | 103 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 tests/qttest/tst_joinedwireuuid.cpp diff --git a/sources/undocommand/deleteqgraphicsitemcommand.cpp b/sources/undocommand/deleteqgraphicsitemcommand.cpp index 521bee8eb..79cd78b2a 100644 --- a/sources/undocommand/deleteqgraphicsitemcommand.cpp +++ b/sources/undocommand/deleteqgraphicsitemcommand.cpp @@ -259,6 +259,22 @@ void DeleteQGraphicsItemCommand::setPotentialsOfRemovedElements() qInfo() << "m_connected_terminals" << m_connected_terminals; Conductor *new_cond = new Conductor(new_pair.first, new_pair.second); new_cond->setProperties(properties); + //The wire that joins the two left by the removed + //symbol gets its uuid from its two ends, as a wire + //saved without one does on load (Diagram::fromXml()): + //the same deletion then saves the same file every + //time, not a random uuid each run (F115) + if (QETProject *wire_project = new_pair.second->diagram() + ? new_pair.second->diagram()->project() : nullptr) { + auto end = [](const Terminal *t) { + return t->parentElement()->uuid().toString() + + QLatin1Char('/') + t->stableUuid().toString(); + }; + QStringList ends{end(new_pair.first), end(new_pair.second)}; + ends.sort(); + new_cond->setUuid(wire_project->derivedItemUuid( + QStringLiteral("conductor"), ends.join(QLatin1Char('\n')))); + } new AddGraphicsObjectCommand(new_cond, new_pair.second->diagram(), QPointF(), this); } } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index a54290356..6965e1b46 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -889,6 +889,19 @@ if(QET_HAS_SCRIPTING) target_compile_definitions(tst_scriptconductoruuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"") + # 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. + add_executable( + tst_joinedwireuuid + tst_joinedwireuuid.cpp) + add_test(NAME tst_joinedwireuuid COMMAND tst_joinedwireuuid) + add_dependencies(tst_joinedwireuuid qelectrotech) + target_link_libraries(tst_joinedwireuuid PRIVATE Qt::Test) + target_compile_definitions(tst_joinedwireuuid PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # qet.terminalPosition() and qet.conductorPath(), checked against each # other on a fixture's conductors. add_executable( diff --git a/tests/qttest/tst_joinedwireuuid.cpp b/tests/qttest/tst_joinedwireuuid.cpp new file mode 100644 index 000000000..0996dcc3d --- /dev/null +++ b/tests/qttest/tst_joinedwireuuid.cpp @@ -0,0 +1,103 @@ +/* + 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 . +*/ +#include +#include +#include +#include + +/** + Deleting a symbol whose terminal held several wires joins the wires + left behind with a new one. That wire got a random uuid, so the same + deletion saved a different file on every run (F115). Its uuid now + comes from its two ends, as a wire saved without a uuid gets on load. + + Runs the real binary's --run on examples/affuteuse_250h.qet (wires + saved without uuids): the coil KA1 on folio 3 deleted, twice, in two + separate runs. Both saves must be identical, and no wire may carry a + random (version 4) uuid. +*/ +class tst_joinedwireuuid : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + QByteArray deleteAndSave(const QString &name) + { + const QString out = m_dir.filePath(name + QStringLiteral(".qet")); + const QString js = m_dir.filePath(name + QStringLiteral(".js")); + QFile script(js); + if (!script.open(QIODevice::WriteOnly)) return {}; + script.write(QStringLiteral( + "qet.log('DELETED ' + qet.deleteElement(3, '{d8ba5ae4-c76f-4936-bb05-f719eec2be81}'));\n" + "qet.save('%1');\n").arg(out).toUtf8()); + script.close(); + + 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), + {QStringLiteral("--run"), js, + QStringLiteral(QET_EXAMPLES_DIR "/affuteuse_250h.qet")}); + if (!proc.waitForFinished(180000) || proc.exitCode() != 0 + || !proc.readAll().contains("DELETED true")) + return {}; + QFile saved(out); + return saved.open(QIODevice::ReadOnly) ? saved.readAll() : QByteArray(); + } + +private slots: + void initTestCase() + { + QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found"); + QVERIFY(m_dir.isValid()); + } + + void sameDeletionSavesTheSameFile() + { + const QByteArray first = deleteAndSave(QStringLiteral("first")); + const QByteArray second = deleteAndSave(QStringLiteral("second")); + QVERIFY2(!first.isEmpty() && !second.isEmpty(), "the coil was not deleted and saved"); + + //No wire with a random uuid: every one is derived + const QRegularExpression wire(QStringLiteral( + "]*\\buuid=\"\\{[0-9a-f]{8}-[0-9a-f]{4}-([0-9a-f])")); + int wires = 0, random = 0; + for (auto it = wire.globalMatch(QString::fromUtf8(first)); it.hasNext(); ++wires) + if (it.next().captured(1) == QLatin1String("4")) ++random; + QVERIFY(wires > 200); + QCOMPARE(random, 0); + + QVERIFY(first == second); + } +}; + +QTEST_GUILESS_MAIN(tst_joinedwireuuid) +#include "tst_joinedwireuuid.moc"