mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-04 18:04:14 +02:00
09380f0ff7
It drives QElectroTech with --run, which such a build does not have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
372 lines
14 KiB
C++
372 lines
14 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#include <QtTest>
|
|
|
|
#include <QDir>
|
|
#include <QDomDocument>
|
|
#include <QFile>
|
|
#include <QProcess>
|
|
#include <QProcessEnvironment>
|
|
#include <QTemporaryDir>
|
|
|
|
#include "wiringrules.h"
|
|
|
|
// The checks that drive QElectroTech through a script (--run) need a
|
|
// build with scripting; without it --run is not an option and the
|
|
// process never exits (QET_HAS_SCRIPTING, top-level CMakeLists.txt).
|
|
#ifdef QET_HAS_SCRIPTING
|
|
# define SKIP_WITHOUT_SCRIPTING
|
|
#else
|
|
# define SKIP_WITHOUT_SCRIPTING QSKIP("built without scripting: --run is not available")
|
|
#endif
|
|
|
|
// How many wires a terminal may take (discussion #1158). The rules are
|
|
// tested on their own; the project setting and the refusal are tested
|
|
// through the real binary: --resave keeps a <wiring_rules> element and
|
|
// adds none to a project without it, and qet.addConductor() is refused
|
|
// past the limit unless the master switch is off.
|
|
class tst_wiringrules : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
QTemporaryDir m_dir;
|
|
int m_run = 0;
|
|
|
|
// Environment of one sandboxed run; @p master_off writes the master
|
|
// switch off into that run's own settings, @p application_max_wires
|
|
// the limit every project follows unless it sets its own.
|
|
QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0)
|
|
{
|
|
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run));
|
|
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run));
|
|
const QString settings = m_dir.filePath(QStringLiteral("settings%1").arg(m_run));
|
|
++m_run;
|
|
QDir().mkpath(home);
|
|
QDir().mkpath(tmp);
|
|
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
|
|
if (master_off || application_max_wires) {
|
|
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
|
|
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
|
|
ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n")
|
|
.arg(master_off ? QStringLiteral("false") : QStringLiteral("true"))
|
|
.arg(application_max_wires).toUtf8());
|
|
}
|
|
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("/.local/share"));
|
|
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings);
|
|
env.insert(QStringLiteral("TMPDIR"), tmp);
|
|
return env;
|
|
}
|
|
|
|
// The fixture with @p rules inserted before <newdiagrams>, as a new file
|
|
QString fixtureWith(const QString &rules)
|
|
{
|
|
QFile source(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
|
|
if (!source.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
return {};
|
|
QString text = QString::fromUtf8(source.readAll());
|
|
const int newdiagrams = text.indexOf(QLatin1String("<newdiagrams"));
|
|
if (newdiagrams < 0)
|
|
return {};
|
|
text.insert(newdiagrams, rules + QStringLiteral("\n "));
|
|
const QString path = m_dir.filePath(QStringLiteral("fixture%1.qet").arg(m_run));
|
|
QFile out(path);
|
|
if (!out.open(QIODevice::WriteOnly | QIODevice::Text))
|
|
return {};
|
|
out.write(text.toUtf8());
|
|
return path;
|
|
}
|
|
|
|
// Runs --resave on @p in, returns the saved file's text (empty on failure)
|
|
QString resave(const QString &in)
|
|
{
|
|
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
|
|
QProcess proc;
|
|
proc.setProcessEnvironment(sandbox());
|
|
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
|
|
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
|
return {};
|
|
QFile file(out);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
return {};
|
|
return QString::fromUtf8(file.readAll());
|
|
}
|
|
|
|
// Wires a free terminal to a terminal that already has a wire, through
|
|
// qet.addConductor(); returns what the script printed after "PROBE ".
|
|
QString addWireToWiredTerminal(const QString &project, bool master_off,
|
|
int application_max_wires = 0)
|
|
{
|
|
const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
|
|
QFile script(script_path);
|
|
if (!script.open(QIODevice::WriteOnly))
|
|
return {};
|
|
// The first end of the first conductor is a terminal with a wire.
|
|
// Any terminal of another element with no wire is the other end.
|
|
script.write(
|
|
"var end = qet.conductorEnds(0, qet.conductorUuids(0)[0])[0].split(' terminal ');\n"
|
|
"var wired = {};\n"
|
|
"qet.conductorUuids(0).forEach(function (u) {\n"
|
|
" qet.conductorEnds(0, u).forEach(function (e) { wired[e] = true; }); });\n"
|
|
"var free = null;\n"
|
|
"qet.elementUuids(0).forEach(function (el) {\n"
|
|
" if (free || el == end[0]) return;\n"
|
|
" var n = qet.elementTerminals(0, el).length;\n"
|
|
" for (var i = 0; i < n && !free; ++i)\n"
|
|
" if (!wired[el + ' terminal ' + i]) free = [el, i]; });\n"
|
|
"qet.log('PROBE ' + (free ? qet.addConductor(0, end[0], parseInt(end[1]), free[0], free[1])\n"
|
|
" : 'nofree'));\n");
|
|
script.close();
|
|
|
|
QProcess proc;
|
|
proc.setProcessEnvironment(sandbox(master_off, application_max_wires));
|
|
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
|
|
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 line.mid(i + mark.size()).trimmed();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// On a fixture with @p rules: a symbol wired to four others is
|
|
// deleted, so QElectroTech rewires the four to keep the potential.
|
|
// Returns the most wires any one of them ends with.
|
|
QString mostWiresAfterDeletingTheHub(const QString &rules)
|
|
{
|
|
const QString project = fixtureWith(rules);
|
|
const QString script_path = m_dir.filePath(QStringLiteral("hub%1.js").arg(m_run));
|
|
QFile script(script_path);
|
|
if (project.isEmpty() || !script.open(QIODevice::WriteOnly))
|
|
return {};
|
|
// Placed on a diagonal, so no two terminals line up and nothing
|
|
// is auto-connected.
|
|
script.write(
|
|
"var p = 'embed://import/probe/v2_fuse.elmt';\n"
|
|
"var hub = qet.addElement(0, p, 400, 400);\n"
|
|
"var others = [];\n"
|
|
"for (var i = 1; i <= 4; ++i) others.push(qet.addElement(0, p, 400 + 70 * i, 400 + 90 * i));\n"
|
|
"others.forEach(function (o) { qet.addConductor(0, hub, 0, o, 0); });\n"
|
|
"qet.deleteElement(0, hub);\n"
|
|
"var count = {};\n"
|
|
"qet.conductorUuids(0).forEach(function (u) {\n"
|
|
" qet.conductorEnds(0, u).forEach(function (e) { count[e] = (count[e] || 0) + 1; }); });\n"
|
|
"var most = 0;\n"
|
|
"others.forEach(function (o) { most = Math.max(most, count[o + ' terminal 0'] || 0); });\n"
|
|
"qet.log('PROBE ' + most);\n");
|
|
script.close();
|
|
|
|
QProcess proc;
|
|
proc.setProcessEnvironment(sandbox());
|
|
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
|
|
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 line.mid(i + mark.size()).trimmed();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
private slots:
|
|
void initTestCase()
|
|
{
|
|
QVERIFY(m_dir.isValid());
|
|
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
|
}
|
|
|
|
void limit()
|
|
{
|
|
WiringRules::Settings rules;
|
|
QCOMPARE(WiringRules::limit(rules, true, false), 0);
|
|
QCOMPARE(WiringRules::limit(rules, true, true), 0);
|
|
|
|
rules.max_wires = 4;
|
|
QCOMPARE(WiringRules::limit(rules, true, false), 4);
|
|
QCOMPARE(WiringRules::limit(rules, true, true), 4);
|
|
|
|
rules.one_wire_per_report = true;
|
|
QCOMPARE(WiringRules::limit(rules, true, true), 1);
|
|
QCOMPARE(WiringRules::limit(rules, true, false), 4);
|
|
|
|
// The master switch turns every rule off
|
|
QCOMPARE(WiringRules::limit(rules, false, false), 0);
|
|
QCOMPARE(WiringRules::limit(rules, false, true), 0);
|
|
}
|
|
|
|
void hasRoom()
|
|
{
|
|
QVERIFY(WiringRules::hasRoom(0, 0));
|
|
QVERIFY(WiringRules::hasRoom(0, 19));
|
|
QVERIFY(WiringRules::hasRoom(2, 1));
|
|
QVERIFY(!WiringRules::hasRoom(2, 2));
|
|
QVERIFY(!WiringRules::hasRoom(2, 3));
|
|
QVERIFY(!WiringRules::hasRoom(1, 1));
|
|
}
|
|
|
|
void chainsOnlyUnderALimit()
|
|
{
|
|
WiringRules::Settings rules;
|
|
QVERIFY(!WiringRules::chainsWires(rules, true));
|
|
rules.one_wire_per_report = true;
|
|
QVERIFY(!WiringRules::chainsWires(rules, true));
|
|
rules.max_wires = 2;
|
|
QVERIFY(WiringRules::chainsWires(rules, true));
|
|
QVERIFY(!WiringRules::chainsWires(rules, false));
|
|
}
|
|
|
|
void chainOrder()
|
|
{
|
|
QCOMPARE(WiringRules::chainOrder({}), QList<int>());
|
|
QCOMPARE(WiringRules::chainOrder({QPointF(5, 5)}), QList<int>({0}));
|
|
|
|
// Starts top left, then always the nearest along the grid
|
|
const QList<QPointF> row {QPointF(300, 0), QPointF(0, 0), QPointF(200, 0), QPointF(100, 0)};
|
|
QCOMPARE(WiringRules::chainOrder(row), QList<int>({1, 3, 2, 0}));
|
|
|
|
// Same x: the higher one starts
|
|
const QList<QPointF> column {QPointF(0, 100), QPointF(0, 0), QPointF(0, 50)};
|
|
QCOMPARE(WiringRules::chainOrder(column), QList<int>({1, 2, 0}));
|
|
|
|
// Each index exactly once
|
|
const QList<QPointF> scattered {QPointF(40, 90), QPointF(10, 300), QPointF(220, 10),
|
|
QPointF(10, 10), QPointF(220, 300)};
|
|
QList<int> order = WiringRules::chainOrder(scattered);
|
|
QCOMPARE(order.first(), 3);
|
|
std::sort(order.begin(), order.end());
|
|
QCOMPARE(order, QList<int>({0, 1, 2, 3, 4}));
|
|
}
|
|
|
|
void projectOverridesApplication()
|
|
{
|
|
WiringRules::Settings application;
|
|
application.max_wires = 2;
|
|
application.one_wire_per_report = true;
|
|
|
|
// A project that follows the application gets its rules
|
|
WiringRules::Settings follows;
|
|
QCOMPARE(WiringRules::effective(follows, application).max_wires, 2);
|
|
QVERIFY(WiringRules::effective(follows, application).one_wire_per_report);
|
|
QVERIFY(!WiringRules::effective(follows, application).own);
|
|
|
|
// Its own rules win, "no limit" included
|
|
WiringRules::Settings own;
|
|
own.own = true;
|
|
QCOMPARE(WiringRules::effective(own, application).max_wires, 0);
|
|
QVERIFY(!WiringRules::effective(own, application).one_wire_per_report);
|
|
QVERIFY(WiringRules::effective(own, application).own);
|
|
own.max_wires = 6;
|
|
QCOMPARE(WiringRules::effective(own, application).max_wires, 6);
|
|
}
|
|
|
|
void xmlRoundTrip()
|
|
{
|
|
QDomDocument doc;
|
|
QDomElement root = doc.createElement(QStringLiteral("project"));
|
|
doc.appendChild(root);
|
|
|
|
// Following the application: nothing written, and reading
|
|
// nothing follows the application
|
|
WiringRules::Settings follows;
|
|
follows.max_wires = 3; // not the project's: not written
|
|
WiringRules::toXml(follows, root);
|
|
QVERIFY(root.firstChildElement().isNull());
|
|
QVERIFY(WiringRules::fromXml(root).isDefault());
|
|
|
|
// Its own rules, even "none", are written and read back as its own
|
|
WiringRules::Settings none;
|
|
none.own = true;
|
|
WiringRules::toXml(none, root);
|
|
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
|
|
QVERIFY(WiringRules::fromXml(root) == none);
|
|
root.removeChild(root.firstChildElement());
|
|
|
|
WiringRules::Settings rules;
|
|
rules.own = true;
|
|
rules.max_wires = 2;
|
|
rules.one_wire_per_report = true;
|
|
WiringRules::toXml(rules, root);
|
|
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
|
|
QVERIFY(WiringRules::fromXml(root) == rules);
|
|
}
|
|
|
|
void savedSettingSurvivesResave()
|
|
{
|
|
const QString plain = resave(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
|
|
QVERIFY2(!plain.isEmpty(), "--resave failed");
|
|
QVERIFY(!plain.contains(QLatin1String("wiring_rules")));
|
|
|
|
const QString with_rules = fixtureWith(
|
|
QStringLiteral("<wiring_rules max_wires_per_terminal=\"2\" one_wire_per_report=\"true\"/>"));
|
|
QVERIFY(!with_rules.isEmpty());
|
|
const QString saved = resave(with_rules);
|
|
QVERIFY2(!saved.isEmpty(), "--resave failed");
|
|
QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\""))
|
|
&& saved.contains(QLatin1String("one_wire_per_report=\"true\"")),
|
|
"the setting was lost on save");
|
|
|
|
// A project's own "no rule" is kept too: it overrides the application's
|
|
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
|
|
QVERIFY(!own_none.isEmpty());
|
|
const QString saved_none = resave(own_none);
|
|
QVERIFY2(!saved_none.isEmpty(), "--resave failed");
|
|
QVERIFY2(saved_none.contains(QLatin1String("<wiring_rules/>")),
|
|
"a project's own rules were lost on save");
|
|
}
|
|
|
|
void wirePastTheLimitIsRefused()
|
|
{
|
|
SKIP_WITHOUT_SCRIPTING;
|
|
// Without a rule the wire is drawn, as on master
|
|
QCOMPARE(addWireToWiredTerminal(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"), false),
|
|
QStringLiteral("true"));
|
|
|
|
// One wire per terminal: the terminal is full
|
|
const QString limited = fixtureWith(
|
|
QStringLiteral("<wiring_rules max_wires_per_terminal=\"1\"/>"));
|
|
QVERIFY(!limited.isEmpty());
|
|
QCOMPARE(addWireToWiredTerminal(limited, false), QStringLiteral("false"));
|
|
|
|
// The master switch off: the project's rule does nothing
|
|
QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true"));
|
|
|
|
// The application's limit applies to a project that sets none...
|
|
const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
|
|
QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false"));
|
|
// ...not to one that sets its own, here "no limit"
|
|
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
|
|
QVERIFY(!own_none.isEmpty());
|
|
QCOMPARE(addWireToWiredTerminal(own_none, false, 1), QStringLiteral("true"));
|
|
// ...and the master switch still turns it off
|
|
QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true"));
|
|
}
|
|
|
|
void deletingASymbolChainsTheWires()
|
|
{
|
|
SKIP_WITHOUT_SCRIPTING;
|
|
// No rule: the four are wired to one of them, as on master
|
|
QCOMPARE(mostWiresAfterDeletingTheHub(QString()), QStringLiteral("3"));
|
|
|
|
// A limit: one after another, none gets more than two
|
|
QCOMPARE(mostWiresAfterDeletingTheHub(
|
|
QStringLiteral("<wiring_rules max_wires_per_terminal=\"4\"/>")),
|
|
QStringLiteral("2"));
|
|
}
|
|
};
|
|
|
|
QTEST_GUILESS_MAIN(tst_wiringrules)
|
|
|
|
#include "tst_wiringrules.moc"
|