Files
qelectrotech-source-mirror/tests/qttest/tst_wiringrules.cpp
T
ispyisail 20e134f2bd Add a wires-per-terminal limit, with an application master switch
Discussion #1158: a real terminal takes one or two wires (four with
double ferrules), a folio report is a virtual point that carries one, but
QElectroTech lets any number of wires be connected anywhere.

Project properties > General gets a "Conducteurs par borne" group:
- the most wires a terminal may take (0, the default, is no limit);
- "one wire per folio report".
When set, a new wire that would go past the limit is refused: the
terminal shows red while dragging, as for any refused link, and a tooltip
says why on release. Auto-connect on placing, moving or multi-pasting a
symbol skips full terminals, and qet.addConductor() refuses with its own
message. Wires already drawn, opening, pasting and undo are never
refused.

Settings > General gets a master switch, on by default, that turns every
rule of this feature off in every project; the project group is greyed
with a note while it is off.

The rules live in wiringrules.cpp. The project setting is saved as
<wiring_rules .../> only when a rule is on, so a project that never used
it saves exactly as before; an older version drops it, which means "off".

tst_wiringrules covers the rules, the save round trip and, through the
real binary, that a wire past the limit is refused unless the master
switch is off (checked to fail with the limit check removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
2026-10-03 21:42:12 +13:00

218 lines
7.8 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"
// 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.
QProcessEnvironment sandbox(bool master_off = false)
{
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) {
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
ini.write("[diagrameditor]\nwiring_rules_enabled=false\n");
}
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)
{
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));
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 xmlRoundTrip()
{
QDomDocument doc;
QDomElement root = doc.createElement(QStringLiteral("project"));
doc.appendChild(root);
// No rule: nothing written, and reading nothing gives no rule
WiringRules::toXml(WiringRules::Settings(), root);
QVERIFY(root.firstChildElement().isNull());
QVERIFY(WiringRules::fromXml(root).isDefault());
WiringRules::Settings rules;
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");
}
void wirePastTheLimitIsRefused()
{
// 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"));
}
};
QTEST_GUILESS_MAIN(tst_wiringrules)
#include "tst_wiringrules.moc"