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
This commit is contained in:
ispyisail
2026-10-03 21:42:12 +13:00
parent 6144da0f91
commit 20e134f2bd
15 changed files with 615 additions and 3 deletions
+2
View File
@@ -179,6 +179,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/wiringlistexport.cpp
${QET_DIR}/sources/wirehops.h
${QET_DIR}/sources/wirehops.cpp
${QET_DIR}/sources/wiringrules.h
${QET_DIR}/sources/wiringrules.cpp
${QET_DIR}/sources/conductorrouter.h
${QET_DIR}/sources/conductorrouter.cpp
${QET_DIR}/sources/ui/wiringlistdialog.h
+5 -2
View File
@@ -1345,7 +1345,8 @@ bool Element::removeTextFromGroup(DynamicElementTextItem *text,
The first Terminal of QPair is a Terminal owned by this element,
this terminal haven't got any conductor docked.
The second Terminal of QPair is a Terminal owned by an other element,
which is aligned with the first Terminal. The second Terminal can have or not docked conductors.
which is aligned with the first Terminal. The second Terminal can have or not docked conductors,
but no more than the project's wires-per-terminal limit allows (discussion #1158).
*/
QList <QPair <Terminal *, Terminal *> > Element::AlignedFreeTerminals() const
{
@@ -1357,7 +1358,9 @@ QList <QPair <Terminal *, Terminal *> > Element::AlignedFreeTerminals() const
{
Terminal *other_terminal =
terminal -> alignedWithTerminal();
if (other_terminal)
if (other_terminal
&& terminal->hasRoomForWire()
&& other_terminal->hasRoomForWire())
list << qMakePair(terminal, other_terminal);
}
}
+51 -1
View File
@@ -26,6 +26,9 @@
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "conductortextitem.h"
#include "../wiringrules.h"
#include <QToolTip>
#include <QtCore/qnumeric.h>
#include <utility>
@@ -610,7 +613,22 @@ void Terminal::mouseReleaseEvent(QGraphicsSceneMouseEvent *e)
other_terminal -> m_hovered = false;
//We stop her if we can't link this terminal with other terminal
if (!canBeLinkedTo(other_terminal)) return;
if (!canBeLinkedTo(other_terminal))
{
//Say why when it is the wire limit (discussion #1158): a red
//terminal alone does not tell the user what to do instead.
const Terminal *full = !hasRoomForWire() ? this
: !other_terminal->hasRoomForWire() ? other_terminal
: nullptr;
if (full) {
QToolTip::showText(e->screenPos(),
tr("Cette borne a déjà %n conducteur(s), la limite du projet. "
"Ajoutez une borne pour raccorder un conducteur de plus.",
"wire refused by the wires-per-terminal limit",
full->wireLimit()));
}
return;
}
//Create conductor
Conductor *new_conductor = new Conductor(this, other_terminal);
@@ -692,6 +710,7 @@ bool Terminal::isLinkedTo(Terminal *other_terminal) {
Reasons for not linable:
- \p other_terminal is this terminal
- this terminal is already connected to \p other_terminal
- either terminal already has as many wires as the project allows
@param other_terminal
@return true if this terminal can be linked to other_terminal,
otherwise false
@@ -701,9 +720,40 @@ bool Terminal::canBeLinkedTo(Terminal *other_terminal)
if (other_terminal == this || isLinkedTo(other_terminal))
return false;
if (!hasRoomForWire() || !other_terminal->hasRoomForWire())
return false;
return true;
}
/**
@brief Terminal::wireLimit
@return the most wires this terminal may take under its project's
wiring rules (discussion #1158), 0 for no limit.
*/
int Terminal::wireLimit() const
{
const Diagram *parent_diagram = diagram();
if (!parent_diagram || !parent_diagram->project()) {
return 0;
}
const Element *element = parentElement();
const bool is_report = element
&& (element->linkType() & (Element::NextReport | Element::PreviousReport));
return WiringRules::limit(parent_diagram->project()->wiringRules(),
WiringRules::masterEnabled(),
is_report);
}
/**
@brief Terminal::hasRoomForWire
@return true if one more wire may be connected to this terminal
*/
bool Terminal::hasRoomForWire() const
{
return WiringRules::hasRoom(wireLimit(), conductorsCount());
}
/**
@brief Terminal::conductors
@return La liste des conducteurs lies a cette borne
+2
View File
@@ -92,6 +92,8 @@ class Terminal : public QGraphicsObject
void updateConductor();
bool isLinkedTo(Terminal *);
bool canBeLinkedTo(Terminal *);
int wireLimit() const;
bool hasRoomForWire() const;
// methods related to XML import/export
static bool valideXml(QDomElement &);
+43
View File
@@ -1257,6 +1257,26 @@ void QETProject::setWireHops(WireHops::Mode mode)
}
}
/**
@brief QETProject::wiringRules
@return how many wires a terminal of this project may take
(discussion #1158); the default when the project sets no rule.
*/
WiringRules::Settings QETProject::wiringRules() const {
return m_wiring_rules;
}
/**
@brief QETProject::setWiringRules
Set how many wires a terminal of this project may take. Only wires
drawn from now on are affected: none already drawn is removed.
@param rules
*/
void QETProject::setWiringRules(const WiringRules::Settings &rules)
{
m_wiring_rules = rules;
}
/**
@brief QETProject::autoBreakConductor
@return true if use of auto break conductor is authorized.
@@ -1367,6 +1387,7 @@ QDomDocument QETProject::toXml()
// local, non-transmitted usage tracking (time spent on this project)
writeUsageXml(project_root);
writeWireHopsXml(project_root);
writeWiringRulesXml(project_root);
// Properties for news diagrams
QDomElement new_diagrams_properties = xml_doc.createElement("newdiagrams");
@@ -1877,6 +1898,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
//Load the local, non-transmitted usage tracking
readUsageXml(xml_project);
readWireHopsXml(xml_project);
readWiringRulesXml(xml_project);
//Load the default properties for the new diagrams
readDefaultPropertiesXml(xml_project);
@@ -2070,6 +2092,17 @@ void QETProject::readWireHopsXml(QDomDocument &xml_project)
: WireHops::fromString(crossings.attribute(QStringLiteral("hop")));
}
/**
@brief QETProject::readWiringRulesXml
Read the <wiring_rules> element of the project, if any.
A project without it sets no rule.
@param xml_project : the xml description of the project
*/
void QETProject::readWiringRulesXml(QDomDocument &xml_project)
{
m_wiring_rules = WiringRules::fromXml(xml_project.documentElement());
}
/**
@brief QETProject::readDefaultPropertiesXml
load default properties for new diagram, found in the xml of this project
@@ -2232,6 +2265,16 @@ void QETProject::writeWireHopsXml(QDomElement &xml_element)
xml_element.appendChild(crossings);
}
/**
@brief QETProject::writeWiringRulesXml
Export the project's wiring rules as a <wiring_rules> child of
\a xml_element, only when one is set.
*/
void QETProject::writeWiringRulesXml(QDomElement &xml_element)
{
WiringRules::toXml(m_wiring_rules, xml_element);
}
/**
@brief QETProject::writeDefaultPropertiesXml
Export all defaults properties used by a new diagram and his content
+6
View File
@@ -29,6 +29,7 @@
#include "titleblock/templatescollection.h"
#include "titleblockproperties.h"
#include "wirehops.h"
#include "wiringrules.h"
#include "diagram.h"
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
@@ -211,6 +212,8 @@ class QETProject : public QObject
void setAutoConductor (bool ac);
WireHops::Mode wireHops() const;
void setWireHops(WireHops::Mode mode);
WiringRules::Settings wiringRules() const;
void setWiringRules(const WiringRules::Settings &rules);
void setAutoBreakConductor (bool abc);
void setAutoElement (bool ae);
void autoFolioNumberingNewFolios ();
@@ -308,11 +311,13 @@ class QETProject : public QObject
void readTerminalStripXml(const QDomDocument &xml_project);
void readUsageXml(QDomDocument &xml_project);
void readWireHopsXml(QDomDocument &xml_project);
void readWiringRulesXml(QDomDocument &xml_project);
void writeProjectPropertiesXml(QDomElement &);
void writeDefaultPropertiesXml(QDomElement &);
void writeUsageXml(QDomElement &);
void writeWireHopsXml(QDomElement &);
void writeWiringRulesXml(QDomElement &);
void addDiagram(Diagram *diagram, int pos = -1);
void detachDiagram(Diagram *diagram);
void writeBackup();
@@ -371,6 +376,7 @@ class QETProject : public QObject
QString m_current_element_autonum;
bool m_auto_conductor = true;
WireHops::Mode m_wire_hops = WireHops::Mode::None;
WiringRules::Settings m_wiring_rules;
bool m_auto_break_conductor = false;
XmlElementCollection *m_elements_collection = nullptr;
bool m_freeze_new_elements = false;
+5
View File
@@ -963,6 +963,11 @@ bool QetScriptApi::addConductor(int folioIndex,
log(QStringLiteral("qet.addConductor: those two terminals are already wired together"));
return false;
}
if (!t1->hasRoomForWire() || !t2->hasRoomForWire()) {
log(QStringLiteral("qet.addConductor: a terminal already has as many wires as the "
"project's wires-per-terminal limit allows"));
return false;
}
if (!t1->canBeLinkedTo(t2)) {
log(QStringLiteral("qet.addConductor: those two terminals cannot be linked"));
return false;
@@ -27,6 +27,7 @@
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include "../../textgrid.h"
#include "../../wiringrules.h"
#include "../../editor/terminalnamecheck.h"
#include "../../ElementsCollection/qetlabelsfile.h"
#include "../prefixconfigurationdialog.h"
@@ -113,6 +114,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_use_windows_mode_rb->setChecked(true);
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
ui->m_conductor_properties_panel->setChecked(settings.value("diagrameditor/conductor_properties_panel", false).toBool());
ui->m_wiring_rules_cb->setChecked(WiringRules::masterEnabled());
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
@@ -334,6 +336,7 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("diagrameditor/highlight-integrated-elements", ui->m_highlight_integrated_elements->isChecked());
settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked());
settings.setValue("diagrameditor/conductor_properties_panel", ui->m_conductor_properties_panel->isChecked());
WiringRules::setMasterEnabled(ui->m_wiring_rules_cb->isChecked());
settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value());
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
@@ -70,6 +70,19 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_wiring_rules_cb">
<property name="toolTip">
<string>Décoché, aucune règle de conducteurs par borne n'est appliquée, quels que soient les réglages des projets.</string>
</property>
<property name="text">
<string>Appliquer les règles de conducteurs par borne des projets</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
@@ -1297,6 +1310,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<tabstop>m_use_gesture_trackpad</tabstop>
<tabstop>m_zoom_out_beyond_folio</tabstop>
<tabstop>m_conductor_properties_panel</tabstop>
<tabstop>m_wiring_rules_cb</tabstop>
<tabstop>m_use_windows_mode_rb</tabstop>
<tabstop>m_use_tab_mode_rb</tabstop>
<tabstop>m_save_label_paste</tabstop>
@@ -26,6 +26,7 @@
#include "../qet.h"
#include "../qeticons.h"
#include "../qetproject.h"
#include "../wiringrules.h"
#include "../borderpropertieswidget.h"
#include "../conductorpropertieswidget.h"
#include "../diagramcontextwidget.h"
@@ -176,6 +177,14 @@ void ProjectMainConfigPage::applyProjectConf()
modified_project = true;
}
WiringRules::Settings wiring_rules = m_project -> wiringRules();
wiring_rules.max_wires = max_wires_sb_ -> value();
wiring_rules.one_wire_per_report = one_wire_per_report_cb_ -> isChecked();
if (m_project -> wiringRules() != wiring_rules) {
m_project -> setWiringRules(wiring_rules);
modified_project = true;
}
if (modified_project) {
m_project -> setModified(true);
}
@@ -225,6 +234,25 @@ void ProjectMainConfigPage::initWidgets()
wire_hops_cb_ -> setToolTip(tr("Dessine un petit arc là où deux conducteurs se croisent sans être reliés. "
"Seul le dessin change : aucun élément n'est ajouté et aucun conducteur n'est coupé.",
"tooltip"));
//How many wires a terminal may take (discussion #1158)
wiring_rules_gb_ = new QGroupBox(tr("Conducteurs par borne", "group box title"));
max_wires_label_ = new QLabel(tr("Nombre maximal de conducteurs par borne :", "label when configuring"));
max_wires_sb_ = new QSpinBox();
max_wires_sb_ -> setRange(0, 99);
max_wires_sb_ -> setSpecialValueText(tr("Sans limite", "wires per terminal"));
max_wires_sb_ -> setToolTip(tr("Un nouveau conducteur qui dépasserait ce nombre sur une borne est refusé. "
"Les conducteurs déjà dessinés ne sont pas modifiés. "
"4 correspond à deux embouts doubles, un de chaque côté de la vis.",
"tooltip"));
one_wire_per_report_cb_ = new QCheckBox(tr("Un seul conducteur par renvoi de folio", "checkbox label"));
one_wire_per_report_cb_ -> setToolTip(tr("Un renvoi de folio est un point virtuel : il ne reçoit qu'un conducteur, "
"celui qui continue sur l'autre folio.",
"tooltip"));
wiring_rules_off_label_ = new QLabel(tr("Ces règles sont désactivées pour tous les projets "
"(Configurer QElectroTech > Général).",
"informative label"));
wiring_rules_off_label_ -> setWordWrap(true);
}
/**
@@ -257,6 +285,17 @@ void ProjectMainConfigPage::initLayout()
wire_hops_layout0 -> addWidget(wire_hops_cb_);
wire_hops_layout0 -> addStretch();
main_layout0 -> addLayout(wire_hops_layout0);
main_layout0 -> addSpacing(10);
QVBoxLayout *wiring_rules_layout = new QVBoxLayout(wiring_rules_gb_);
QHBoxLayout *max_wires_layout = new QHBoxLayout();
max_wires_layout -> addWidget(max_wires_label_);
max_wires_layout -> addWidget(max_wires_sb_);
max_wires_layout -> addStretch();
wiring_rules_layout -> addLayout(max_wires_layout);
wiring_rules_layout -> addWidget(one_wire_per_report_cb_);
wiring_rules_layout -> addWidget(wiring_rules_off_label_);
main_layout0 -> addWidget(wiring_rules_gb_);
setLayout(main_layout0);
this -> setMinimumWidth(680);
@@ -280,6 +319,17 @@ void ProjectMainConfigPage::readValuesFromProject()
const int wire_hops_index = wire_hops_cb_ -> findData(WireHops::toString(m_project -> wireHops()));
wire_hops_cb_ -> setCurrentIndex(qMax(0, wire_hops_index));
const WiringRules::Settings wiring_rules = m_project -> wiringRules();
max_wires_sb_ -> setValue(wiring_rules.max_wires);
one_wire_per_report_cb_ -> setChecked(wiring_rules.one_wire_per_report);
//The master switch wins over every project: say so rather than
//let the user set a rule that does nothing.
const bool master = WiringRules::masterEnabled();
wiring_rules_off_label_ -> setVisible(!master);
max_wires_label_ -> setEnabled(master);
max_wires_sb_ -> setEnabled(master);
one_wire_per_report_cb_ -> setEnabled(master);
}
/**
@@ -307,6 +357,7 @@ void ProjectMainConfigPage::adjustReadOnly()
usage_enabled_cb_ -> setDisabled(is_read_only);
usage_reset_pb_ -> setDisabled(is_read_only);
wire_hops_cb_ -> setDisabled(is_read_only);
wiring_rules_gb_ -> setDisabled(is_read_only);
}
//######################################################################################//
@@ -24,6 +24,8 @@ class QComboBox;
class QLineEdit;
class QCheckBox;
class QPushButton;
class QSpinBox;
class QGroupBox;
class QETProject;
class BorderPropertiesWidget;
class ConductorPropertiesWidget;
@@ -128,6 +130,11 @@ class ProjectMainConfigPage : public ProjectConfigPage {
QPushButton *usage_reset_pb_;
QLabel *wire_hops_label_;
QComboBox *wire_hops_cb_;
QGroupBox *wiring_rules_gb_;
QLabel *max_wires_label_;
QSpinBox *max_wires_sb_;
QCheckBox *one_wire_per_report_cb_;
QLabel *wiring_rules_off_label_;
};
class ProjectAutoNumConfigPage : public ProjectConfigPage {
+126
View File
@@ -0,0 +1,126 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "wiringrules.h"
#include <QDomDocument>
#include <QDomElement>
#include <QSettings>
namespace {
const QString element_name = QStringLiteral("wiring_rules");
const QString max_wires_attribute = QStringLiteral("max_wires_per_terminal");
const QString report_attribute = QStringLiteral("one_wire_per_report");
//The master switch is read on every wire drawn, so it is read from
//the settings once and kept; setMasterEnabled() keeps both in step.
int master_cache = -1;
}
/**
@brief WiringRules::masterEnabled
@return false when the user turned every rule of this feature off for
every project (Settings > General). On by default: a project still has
to turn a rule on before anything changes.
*/
bool WiringRules::masterEnabled()
{
if (master_cache < 0) {
master_cache = QSettings().value(settings_key, true).toBool() ? 1 : 0;
}
return master_cache == 1;
}
/**
@brief WiringRules::setMasterEnabled
Save the master switch.
@param enabled
*/
void WiringRules::setMasterEnabled(bool enabled)
{
QSettings().setValue(settings_key, enabled);
master_cache = enabled ? 1 : 0;
}
/**
@brief WiringRules::fromXml
@param project_root : the root element of a project
@return the rules stored in the <wiring_rules> child of \a project_root,
the default (no rule) when there is none.
*/
WiringRules::Settings WiringRules::fromXml(const QDomElement &project_root)
{
Settings settings;
const QDomElement rules = project_root.firstChildElement(element_name);
if (rules.isNull()) {
return settings;
}
settings.max_wires = qMax(0, rules.attribute(max_wires_attribute, QStringLiteral("0")).toInt());
settings.one_wire_per_report = rules.attribute(report_attribute) == QLatin1String("true");
return settings;
}
/**
@brief WiringRules::toXml
Write \a settings as a <wiring_rules> child of \a project_root. Nothing
is written when no rule is on, so a project that never used them saves
exactly as before.
*/
void WiringRules::toXml(const Settings &settings, QDomElement &project_root)
{
if (settings.isDefault()) {
return;
}
QDomElement rules = project_root.ownerDocument().createElement(element_name);
if (settings.max_wires > 0) {
rules.setAttribute(max_wires_attribute, settings.max_wires);
}
if (settings.one_wire_per_report) {
rules.setAttribute(report_attribute, QStringLiteral("true"));
}
project_root.appendChild(rules);
}
/**
@brief WiringRules::limit
@param settings : the project's rules
@param master_enabled : the master switch, masterEnabled()
@param is_report : the terminal belongs to a folio report
@return the most wires the terminal may take, 0 for no limit.
*/
int WiringRules::limit(const Settings &settings, bool master_enabled, bool is_report)
{
if (!master_enabled) {
return 0;
}
if (is_report && settings.one_wire_per_report) {
return 1;
}
return settings.max_wires;
}
/**
@brief WiringRules::hasRoom
@param limit : WiringRules::limit()
@param wires : the wires the terminal already has
@return true if one more wire may be connected
*/
bool WiringRules::hasRoom(int limit, int wires)
{
return limit <= 0 || wires < limit;
}
+68
View File
@@ -0,0 +1,68 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef WIRINGRULES_H
#define WIRINGRULES_H
class QDomElement;
/**
@brief The WiringRules namespace
How many wires a terminal may take (discussion #1158). A real terminal
takes one or two wires, sometimes four with double ferrules; a folio
report is a virtual point that should carry one. When a project sets a
limit, a new wire that would go past it is refused. Wires already in
the drawing are never touched.
Every rule is off unless the project turns it on, and the application
has a master switch (masterEnabled()) that turns all of them off in
every project at once. Kept free of any QGraphicsItem so the rules can
be tested on their own.
*/
namespace WiringRules
{
///The QSettings key of the master switch
constexpr const char *settings_key = "diagrameditor/wiring_rules_enabled";
///The rules a project chose. The default is "no rule".
struct Settings
{
///Most wires a terminal may take, 0 for no limit
int max_wires = 0;
///A folio report takes one wire only
bool one_wire_per_report = false;
bool isDefault() const { return *this == Settings(); }
bool operator==(const Settings &other) const {
return max_wires == other.max_wires
&& one_wire_per_report == other.one_wire_per_report;
}
bool operator!=(const Settings &other) const { return !(*this == other); }
};
bool masterEnabled();
void setMasterEnabled(bool enabled);
Settings fromXml(const QDomElement &project_root);
void toXml(const Settings &settings, QDomElement &project_root);
int limit(const Settings &settings, bool master_enabled, bool is_report);
bool hasRoom(int limit, int wires);
}
#endif // WIRINGRULES_H
+15
View File
@@ -460,6 +460,21 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_wirehops PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# Wires per terminal (discussion #1158): the rules in wiringrules.cpp, and
# through the real binary, that --resave keeps the project setting, adds
# none to a project without it, and that a wire past the limit is refused
# unless the master switch is off.
add_executable(
tst_wiringrules
tst_wiringrules.cpp
${QET_DIR}/sources/wiringrules.cpp)
add_test(NAME tst_wiringrules COMMAND tst_wiringrules)
add_dependencies(tst_wiringrules qelectrotech)
target_include_directories(tst_wiringrules PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_wiringrules PRIVATE Qt::Test Qt::Xml)
target_compile_definitions(tst_wiringrules PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The routing behind qet.routeConductor(): conductorrouter.cpp needs only
# Qt Core, so the geometry is tested without a scene.
add_executable(
+217
View File
@@ -0,0 +1,217 @@
// 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"