From 9fdd6ac3332891ff12d596233a4d319377558bf8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 29 Sep 2026 10:40:11 +1300 Subject: [PATCH 1/2] Scripting: find a terminal by its uuid The conductor calls name a wire end as element uuid + terminal index. The index is the terminal's place in Element::terminals(), a sort by position that is undefined between two terminals at the same point, and the documentation ruled terminal uuids out as "empty for most of the installed base". Since #1118 every terminal of an opened project has one. - elementTerminals() ends each line with the terminal's uuid (Terminal::stableUuid()); the text before it is unchanged. - terminalIndex(folio, elementUuid, terminalUuid) returns the index the calls take, or -1 if the element or terminal is not there, or if two of the element's terminals carry that uuid. - The class documentation says what does address a terminal: its uuid together with its element's. tst_scriptterminaluuid runs --run on perceuse.qet (552 elements, two terminals at one point in some): every terminal listed with a uuid, distinct within its element, found again at its own index; -1 for an unknown or malformed uuid, an unknown element and a bad folio. Red when terminalIndex() returns the wrong index. qet-mcp suite 253/253. Co-Authored-By: Claude Opus 5.5 --- sources/scripting/qetscriptapi.cpp | 53 ++++++++--- sources/scripting/qetscriptapi.h | 20 ++-- tests/qttest/CMakeLists.txt | 13 +++ tests/qttest/tst_scriptterminaluuid.cpp | 121 ++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 tests/qttest/tst_scriptterminaluuid.cpp diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 8cbbd926a..c7344678a 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -739,18 +739,19 @@ QString QetScriptApi::elementName(int folioIndex, const QString &elementUuid) co /** @brief QetScriptApi::elementTerminals The element's terminals, in the order addConductor() indexes them: one - entry per terminal, ": ( conductor(s))". Descriptive - rather than structured because its only job is to let a script -- or a - human reading a script's output -- see which index is which before - wiring anything to it. + entry per terminal, ": ( conductor(s)) ". + Descriptive rather than structured because its only job is to let a + script -- or a human reading a script's output -- see which index is + which before wiring anything to it. - Indexes, not uuids, because a terminal uuid does not address a terminal - on a folio. Terminal::uuid() comes from the catalog .elmt definition - (see Terminal::stableUuid()), so it is empty for most of the installed - base, and where it is not, every instance of that same element carries - the same one -- two coils of one type placed side by side have - byte-identical terminal uuids, which is plainly visible in the saved - file of any project written through this API. + The calls take the index; the uuid is what to hold instead, and + terminalIndex() turns it back into the index. It is Terminal:: + stableUuid(): the terminal's own uuid, which every terminal of an + opened project has (TerminalUuids::fillMissing()), or for a symbol + imported since the project was opened the value the next opening will + give it. It comes from the symbol's definition, so it names a terminal + only together with its element: two coils of one type placed side by + side have the same terminal uuids. The index is the terminal's place in Element::terminals(), and that is @b not the order the .elmt lists them in. Element::parseTerminal() @@ -772,14 +773,40 @@ QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elemen for (int i = 0 ; i < terminals.count() ; ++i) { Terminal *t = terminals.at(i); - list << QStringLiteral("%1: %2 (%3 conductor(s))") + list << QStringLiteral("%1: %2 (%3 conductor(s)) %4") .arg(i) .arg(t->name().isEmpty() ? QStringLiteral("-") : t->name()) - .arg(t->conductorsCount()); + .arg(t->conductorsCount()) + .arg(t->stableUuid().toString()); } return list; } +/** + @brief QetScriptApi::terminalIndex + The current index in elementTerminals() of the terminal of the element + @p elementUuid whose uuid is @p terminalUuid, or -1 -- also when two of + its terminals carry that uuid, since the index would then be a guess. + The index is what addConductor() and the conductor calls take; unlike + the uuid, it is undefined between two terminals at the same point. +*/ +int QetScriptApi::terminalIndex(int folioIndex, const QString &elementUuid, + const QString &terminalUuid) const +{ + Element *element = findElement(folioIndex, elementUuid); + const QUuid wanted(terminalUuid); + if (!element || wanted.isNull()) return -1; + + int found = -1; + const QList terminals = element->terminals(); + for (int i = 0 ; i < terminals.count() ; ++i) { + if (terminals.at(i)->stableUuid() != wanted) continue; + if (found >= 0) return -1; + found = i; + } + return found; +} + QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const { Element *element = findElement(folioIndex, elementUuid); diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 62240935e..dff3225e7 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -96,14 +96,16 @@ class QetGraphicsTableItem; no way to connect them. Terminals are addressed by their @b index in Element::terminals(), - not by uuid, and elementTerminals() prints that indexing so a script - can see what it is about to wire. The index is the terminal's place - in the element's own top-to-bottom, left-to-right ordering, not the - order its definition file lists them. Terminal uuids look like the - obvious key and are not one: Terminal::uuid() is a property of the - catalog .elmt definition, empty for most of the installed base and, - where present, identical across every instance of that element -- so - it does not distinguish one placed coil's A1 from another's. + and elementTerminals() prints that indexing so a script can see what + it is about to wire. The index is the terminal's place in the + element's own top-to-bottom, left-to-right ordering, not the order + its definition file lists them, and it is undefined between two + terminals at the same point. What does not move is the terminal's + uuid together with its element's: each elementTerminals() line ends + with the terminal's uuid, and terminalIndex() turns element uuid + + terminal uuid back into the index. A terminal uuid alone is not an + address -- it comes from the symbol's definition, so every placed + instance of one symbol has the same ones. - @b Conductor properties and @b cross-references: set a conductor's number, formula, colour or section (and its look: style normal/ dashed/dashdotted, two-colour mode and second colour, dash size, @@ -384,6 +386,8 @@ class QetScriptApi : public QObject Q_INVOKABLE QStringList elementUuids(int folioIndex) const; Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const; Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE int terminalIndex(int folioIndex, const QString &elementUuid, + const QString &terminalUuid) const; // -- element information, through ChangeElementInformationCommand -- Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 9656c557f..f3726de04 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -343,6 +343,19 @@ if(QET_HAS_SCRIPTING) target_link_libraries(tst_scriptconductoruuid PRIVATE Qt::Test) target_compile_definitions(tst_scriptconductoruuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"") + + # qet.elementTerminals() lists each terminal's uuid and qet.terminalIndex() + # turns element uuid + terminal uuid back into the index the conductor + # calls take, on examples/perceuse.qet (two terminals at one point). + add_executable( + tst_scriptterminaluuid + tst_scriptterminaluuid.cpp) + add_test(NAME tst_scriptterminaluuid COMMAND tst_scriptterminaluuid) + add_dependencies(tst_scriptterminaluuid qelectrotech) + target_link_libraries(tst_scriptterminaluuid PRIVATE Qt::Test) + target_compile_definitions(tst_scriptterminaluuid PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") endif() # A symbol saved without a uuid gets the same one on every load, and diff --git a/tests/qttest/tst_scriptterminaluuid.cpp b/tests/qttest/tst_scriptterminaluuid.cpp new file mode 100644 index 000000000..ff8bece9c --- /dev/null +++ b/tests/qttest/tst_scriptterminaluuid.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// qet.elementTerminals() ends each line with the terminal's uuid, and +// qet.terminalIndex(folio, element, terminalUuid) turns it back into the +// index the conductor calls take. Runs a script through the real binary's +// --run on perceuse.qet, which has symbols with two terminals at one point: +// their order in the index is undefined, their uuids are not. +class tst_scriptterminaluuid : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Run @p script on @p project in a sandbox of its own and return the + // JSON object it logged. + QJsonObject run(const QString &script, const QString &project) + { + const QString path = m_dir.filePath(QStringLiteral("probe.js")); + const QString home = m_dir.filePath(QStringLiteral("home")); + QDir().mkpath(home); + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) return {}; + f.write(script.toUtf8()); + f.close(); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + 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()); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--run"), path, project}); + if (!proc.waitForFinished(120000)) return {}; + const QString out = QString::fromUtf8(proc.readAllStandardOutput() + + proc.readAllStandardError()); + const QString mark = QStringLiteral("PROBE "); + for (const QString &line : out.split(QLatin1Char('\n'))) { + const int i = line.indexOf(mark); + if (i >= 0) + return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object(); + } + return {}; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void everyTerminalFoundByItsUuid() + { + const QJsonObject r = run(QStringLiteral( + "var rows = [], pick = null;\n" + "for (var f = 0; f < qet.folioCount(); f++) {\n" + " var els = qet.elementUuids(f);\n" + " for (var e = 0; e < els.length; e++) {\n" + " var lines = qet.elementTerminals(f, els[e]);\n" + " var back = lines.map(function (l) {\n" + " return qet.terminalIndex(f, els[e], l.substring(l.lastIndexOf(' ') + 1)); });\n" + " rows.push({lines: lines, back: back});\n" + " if (!pick && lines.length) pick = {f: f, e: els[e], t: lines[0].split(' ').pop()};\n" + " }\n" + "}\n" + "qet.log('PROBE ' + JSON.stringify({rows: rows,\n" + " unknown: qet.terminalIndex(pick.f, pick.e, '{00000000-0000-0000-0000-000000000001}'),\n" + " junk: qet.terminalIndex(pick.f, pick.e, 'not a uuid'),\n" + " noElement: qet.terminalIndex(pick.f, '{00000000-0000-0000-0000-000000000002}', pick.t),\n" + " badFolio: qet.terminalIndex(99, pick.e, pick.t)}));\n"), + QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")); + QVERIFY2(!r.isEmpty(), "the script logged nothing"); + + const QRegularExpression line_re(QStringLiteral( + "^(\\d+): .* \\(\\d+ conductor\\(s\\)\\) (\\{[0-9a-f-]{36}\\})$")); + int terminals = 0; + for (const QJsonValue &row : r.value(QStringLiteral("rows")).toArray()) { + const QJsonArray lines = row.toObject().value(QStringLiteral("lines")).toArray(); + const QJsonArray back = row.toObject().value(QStringLiteral("back")).toArray(); + QCOMPARE(back.size(), lines.size()); + QSet uuids; + for (int i = 0; i < lines.size(); ++i) { + const QString line = lines.at(i).toString(); + const QRegularExpressionMatch m = line_re.match(line); + QVERIFY2(m.hasMatch(), qPrintable(line)); + QCOMPARE(m.captured(1).toInt(), i); + QVERIFY(!QUuid(m.captured(2)).isNull()); + uuids.insert(m.captured(2)); + // the uuid finds this very terminal again + QCOMPARE(back.at(i).toInt(), i); + ++terminals; + } + QCOMPARE(uuids.size(), lines.size()); + } + QVERIFY(terminals > 200); + + QCOMPARE(r.value(QStringLiteral("unknown")).toInt(), -1); + QCOMPARE(r.value(QStringLiteral("junk")).toInt(), -1); + QCOMPARE(r.value(QStringLiteral("noElement")).toInt(), -1); + QCOMPARE(r.value(QStringLiteral("badFolio")).toInt(), -1); + } +}; + +QTEST_APPLESS_MAIN(tst_scriptterminaluuid) + +#include "tst_scriptterminaluuid.moc" From 3112727ee737363c612d52298872b4256e2d17a9 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 29 Sep 2026 10:49:24 +1300 Subject: [PATCH 2/2] elementTerminals(): keep a "%3" or "%4" in a terminal's name as written Review of the previous commit: the line was built with chained arg() calls, so a "%3" in a terminal's name was replaced by the conductor count -- as before -- and now a "%4" by the uuid as well. One multi-argument arg() substitutes each placeholder of the pattern once. percentInNameKept renames a terminal of perceuse.qet "x%3y%4": listed as "x1y{uuid}" before, "x%3y%4" now. Co-Authored-By: Claude Opus 5.5 --- sources/scripting/qetscriptapi.cpp | 10 ++++--- tests/qttest/CMakeLists.txt | 2 +- tests/qttest/tst_scriptterminaluuid.cpp | 36 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index c7344678a..53f3a41aa 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -773,11 +773,13 @@ QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elemen for (int i = 0 ; i < terminals.count() ; ++i) { Terminal *t = terminals.at(i); + //One multi-argument arg(): chained ones would also replace a + //"%3" or "%4" inside the terminal's name list << QStringLiteral("%1: %2 (%3 conductor(s)) %4") - .arg(i) - .arg(t->name().isEmpty() ? QStringLiteral("-") : t->name()) - .arg(t->conductorsCount()) - .arg(t->stableUuid().toString()); + .arg(QString::number(i), + t->name().isEmpty() ? QStringLiteral("-") : t->name(), + QString::number(t->conductorsCount()), + t->stableUuid().toString()); } return list; } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index f3726de04..e10e2ad16 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -352,7 +352,7 @@ if(QET_HAS_SCRIPTING) tst_scriptterminaluuid.cpp) add_test(NAME tst_scriptterminaluuid COMMAND tst_scriptterminaluuid) add_dependencies(tst_scriptterminaluuid qelectrotech) - target_link_libraries(tst_scriptterminaluuid PRIVATE Qt::Test) + target_link_libraries(tst_scriptterminaluuid PRIVATE Qt::Test Qt::Xml) target_compile_definitions(tst_scriptterminaluuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") diff --git a/tests/qttest/tst_scriptterminaluuid.cpp b/tests/qttest/tst_scriptterminaluuid.cpp index ff8bece9c..316282493 100644 --- a/tests/qttest/tst_scriptterminaluuid.cpp +++ b/tests/qttest/tst_scriptterminaluuid.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -114,6 +115,41 @@ private slots: QCOMPARE(r.value(QStringLiteral("noElement")).toInt(), -1); QCOMPARE(r.value(QStringLiteral("badFolio")).toInt(), -1); } + + // A "%3" or "%4" in a terminal's name is listed as written, not + // replaced by the conductor count or the uuid. + void percentInNameKept() + { + QFile in(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")); + QVERIFY(in.open(QIODevice::ReadOnly)); + QDomDocument doc; + QVERIFY(bool(doc.setContent(&in))); + const QDomNodeList terminals = doc.documentElement() + .firstChildElement(QStringLiteral("collection")) + .elementsByTagName(QStringLiteral("terminal")); + QVERIFY(!terminals.isEmpty()); + terminals.at(0).toElement().setAttribute(QStringLiteral("name"), + QStringLiteral("x%3y%4")); + const QString project = m_dir.filePath(QStringLiteral("percent.qet")); + QFile out(project); + QVERIFY(out.open(QIODevice::WriteOnly)); + out.write(doc.toByteArray()); + out.close(); + + const QJsonObject r = run(QStringLiteral( + "var hits = [];\n" + "for (var f = 0; f < qet.folioCount(); f++) {\n" + " var els = qet.elementUuids(f);\n" + " for (var e = 0; e < els.length; e++)\n" + " qet.elementTerminals(f, els[e]).forEach(function (l) {\n" + " if (/: x.*y.* \\(/.test(l)) hits.push(l); });\n" + "}\n" + "qet.log('PROBE ' + JSON.stringify({hits: hits}));\n"), project); + const QJsonArray hits = r.value(QStringLiteral("hits")).toArray(); + QVERIFY2(!hits.isEmpty(), "the renamed terminal was not listed"); + for (const QJsonValue &h : hits) + QVERIFY2(h.toString().contains(QStringLiteral(": x%3y%4 (")), qPrintable(h.toString())); + } }; QTEST_APPLESS_MAIN(tst_scriptterminaluuid)