From 353dbd33175347f4df7fe87b62a0382c3ace5209 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 28 Sep 2026 21:07:16 +1300 Subject: [PATCH] Scripting: list a folio's conductors by uuid and find their ends A script could name a conductor only by one of its ends, "the conductor on terminal N of element X", which fails where two conductors meet at one terminal and cannot follow a conductor that is re-connected. qet.conductorUuids(folio) lists the folio's conductor uuids, in the order qet.conductors() lists them. qet.conductorEnds(folio, uuid) returns that conductor's two ends as "{element uuid} terminal N" -- the form conductors() prints and the conductor calls take -- or an empty list if the folio has no such conductor. The end formatting conductors() already did is shared rather than copied. Conductors of older projects have no saved uuid yet, so theirs change from one load to the next until that is settled (discussion #1103); new conductors keep theirs. tst_scriptconductoruuid runs a script through --run on a fixture: every conductor has a distinct uuid, and its ends match the conductors() line at the same position; an unknown uuid, a malformed one and a folio that does not exist give empty lists. It fails with the two ends swapped. Co-Authored-By: Claude Opus 5.5 --- sources/scripting/qetscriptapi.cpp | 62 ++++++++++++-- sources/scripting/qetscriptapi.h | 2 + tests/qttest/CMakeLists.txt | 12 +++ tests/qttest/tst_scriptconductoruuid.cpp | 104 +++++++++++++++++++++++ 4 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 tests/qttest/tst_scriptconductoruuid.cpp diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index b3d083a3f..b88d8ee17 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -844,6 +844,18 @@ bool QetScriptApi::addConductor(int folioIndex, exists so a script, or a person reading its output, can see what is there before changing it. */ +namespace { +/// "{element uuid} terminal N", the form conductors() prints an end in and +/// the conductor calls take as element uuid + terminal index. +QString describeEnd(Terminal *t) +{ + if (!t || !t->parentElement()) return QStringLiteral("?"); + return QStringLiteral("%1 terminal %2") + .arg(t->parentElement()->uuid().toString()) + .arg(t->parentElement()->terminals().indexOf(t)); +} +} // namespace + QStringList QetScriptApi::conductors(int folioIndex) const { QStringList list; @@ -851,23 +863,57 @@ QStringList QetScriptApi::conductors(int folioIndex) const const QList diagrams = m_project->diagrams(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; - auto describe = [](Terminal *t) -> QString { - if (!t || !t->parentElement()) return QStringLiteral("?"); - return QStringLiteral("%1 terminal %2") - .arg(t->parentElement()->uuid().toString()) - .arg(t->parentElement()->terminals().indexOf(t)); - }; - DiagramContent content(diagrams.at(folioIndex), false); const QList all = content.conductors(DiagramContent::AnyConductor); for (Conductor *c : all) { list << QStringLiteral("%1 -- %2 : num='%3'") - .arg(describe(c->terminal1), describe(c->terminal2), c->properties().text); + .arg(describeEnd(c->terminal1), describeEnd(c->terminal2), c->properties().text); } return list; } +/** + @brief QetScriptApi::conductorUuids + The uuid of every conductor on the folio, in the order conductors() + lists them. +*/ +QStringList QetScriptApi::conductorUuids(int folioIndex) const +{ + QStringList list; + if (!m_project) return list; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; + + DiagramContent content(diagrams.at(folioIndex), false); + for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) + list << c->uuid().toString(); + return list; +} + +/** + @brief QetScriptApi::conductorEnds + The two ends of the conductor carrying @p uuid, each as + "{element uuid} terminal N" -- the element uuid and terminal index the + conductor calls take -- or an empty list if the folio has no such + conductor. A uuid names one conductor even where two meet at a terminal, + which an element uuid + terminal index cannot. +*/ +QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const +{ + if (!m_project) return {}; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; + const QUuid wanted(uuid); + if (wanted.isNull()) return {}; + + DiagramContent content(diagrams.at(folioIndex), false); + for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) + if (c->uuid() == wanted) + return {describeEnd(c->terminal1), describeEnd(c->terminal2)}; + return {}; +} + QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &property) const { diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 533b70830..edaea2e80 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -396,6 +396,8 @@ class QetScriptApi : public QObject // -- conductor properties, applied to the whole potential -- Q_INVOKABLE QStringList conductors(int folioIndex) const; + Q_INVOKABLE QStringList conductorUuids(int folioIndex) const; + Q_INVOKABLE QStringList conductorEnds(int folioIndex, const QString &uuid) const; Q_INVOKABLE QString conductorProperty(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &property) const; Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid, diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 94283cc09..64a3b8cd0 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -328,3 +328,15 @@ target_include_directories(tst_conductorselfretrace PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test) target_compile_definitions(tst_conductorselfretrace PRIVATE "QET_TEST_BINARY_PATH=\"$\"") + +# qet.conductorUuids() / qet.conductorEnds(): a script lists a folio's +# conductors by uuid and finds each one's two ends. Runs a script through +# the real binary's --run on fixtures/qet_bug_repro_resaved.qet. +add_executable( + tst_scriptconductoruuid + tst_scriptconductoruuid.cpp) +add_test(NAME tst_scriptconductoruuid COMMAND tst_scriptconductoruuid) +add_dependencies(tst_scriptconductoruuid qelectrotech) +target_link_libraries(tst_scriptconductoruuid PRIVATE Qt::Test) +target_compile_definitions(tst_scriptconductoruuid PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") diff --git a/tests/qttest/tst_scriptconductoruuid.cpp b/tests/qttest/tst_scriptconductoruuid.cpp new file mode 100644 index 000000000..27792ab70 --- /dev/null +++ b/tests/qttest/tst_scriptconductoruuid.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// qet.conductorUuids(folio) and qet.conductorEnds(folio, uuid): a script can +// list a folio's conductors by uuid and find where each one runs, in the +// "{element} terminal N" form conductors() prints and the conductor calls +// take. Runs a script through the real binary's --run. +class tst_scriptconductoruuid : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Run @p script on the fixture in a sandbox of its own and return the + // JSON object it logged. + QJsonObject run(const QString &script) + { + 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")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--run"), path, + QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet")}); + if (!proc.waitForFinished(60000)) 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 uuidsAndEndsMatchConductors() + { + const QJsonObject r = run(QStringLiteral( + "var uuids = qet.conductorUuids(0);\n" + "var ends = uuids.map(function (u) { return qet.conductorEnds(0, u); });\n" + "qet.log('PROBE ' + JSON.stringify({lines: qet.conductors(0), uuids: uuids, ends: ends,\n" + " unknown: qet.conductorEnds(0, '{00000000-0000-0000-0000-000000000001}'),\n" + " junk: qet.conductorEnds(0, 'not a uuid'),\n" + " badFolio: qet.conductorUuids(99)}));\n")); + QVERIFY2(!r.isEmpty(), "the script logged nothing"); + + const QJsonArray lines = r.value(QStringLiteral("lines")).toArray(); + const QJsonArray uuids = r.value(QStringLiteral("uuids")).toArray(); + const QJsonArray ends = r.value(QStringLiteral("ends")).toArray(); + QCOMPARE(lines.size(), 7); // the fixture's conductors + QCOMPARE(uuids.size(), lines.size()); + QSet distinct; + for (int i = 0; i < uuids.size(); ++i) { + const QString u = uuids.at(i).toString(); + QVERIFY2(!QUuid(u).isNull(), qPrintable(u)); + distinct.insert(u); + // same order as conductors(), and the same two ends it prints + const QJsonArray e = ends.at(i).toArray(); + QCOMPARE(e.size(), 2); + const QString expected = e.at(0).toString() + QStringLiteral(" -- ") + + e.at(1).toString() + QStringLiteral(" : "); + QVERIFY2(lines.at(i).toString().startsWith(expected), + qPrintable(lines.at(i).toString() + QStringLiteral(" | ") + expected)); + } + QCOMPARE(distinct.size(), uuids.size()); + + QVERIFY(r.value(QStringLiteral("unknown")).toArray().isEmpty()); + QVERIFY(r.value(QStringLiteral("junk")).toArray().isEmpty()); + QVERIFY(r.value(QStringLiteral("badFolio")).toArray().isEmpty()); + } +}; + +QTEST_APPLESS_MAIN(tst_scriptconductoruuid) + +#include "tst_scriptconductoruuid.moc"