Merge pull request #1125 from ispyisail/feat/script-terminal-uuid

Add a scripting lookup for terminals by uuid
This commit is contained in:
ispyisail
2026-09-29 11:36:31 +13:00
committed by GitHub
4 changed files with 226 additions and 23 deletions
+44 -15
View File
@@ -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, "<index>: <name> (<n> 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, "<index>: <name> (<n> conductor(s)) <uuid>".
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,42 @@ 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))")
.arg(i)
.arg(t->name().isEmpty() ? QStringLiteral("-") : t->name())
.arg(t->conductorsCount());
//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(QString::number(i),
t->name().isEmpty() ? QStringLiteral("-") : t->name(),
QString::number(t->conductorsCount()),
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<Terminal *> 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);
+12 -8
View File
@@ -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;
+13
View File
@@ -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=\"$<TARGET_FILE:qelectrotech>\"")
# 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 Qt::Xml)
target_compile_definitions(tst_scriptterminaluuid PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
endif()
# A symbol saved without a uuid gets the same one on every load, and
+157
View File
@@ -0,0 +1,157 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QFile>
#include <QDomDocument>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QUuid>
// 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<QString> 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);
}
// 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)
#include "tst_scriptterminaluuid.moc"