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"