mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-30 23:04:13 +02:00
Merge pull request #1159 from ispyisail/feature/terminal-name-check
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m44s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m44s
Add a terminal name check when saving a symbol (#1157)
This commit is contained in:
+24
-1
@@ -24,6 +24,7 @@
|
||||
#include "dataBase/projectdatabase.h"
|
||||
#include "diagram.h"
|
||||
#include "diagramcontext.h"
|
||||
#include "editor/terminalnamecheck.h"
|
||||
#include "dxfexport.h"
|
||||
#include "exportproperties.h"
|
||||
#include "pdf_links.h"
|
||||
@@ -513,7 +514,20 @@ int checkOneElement(const QString &path)
|
||||
return 2;
|
||||
}
|
||||
|
||||
const int terminals = root.elementsByTagName("terminal").count();
|
||||
const QDomNodeList terminal_nodes = root.elementsByTagName("terminal");
|
||||
const int terminals = terminal_nodes.count();
|
||||
QStringList terminal_names;
|
||||
for (int i = 0; i < terminals; ++i)
|
||||
terminal_names << terminal_nodes.at(i).toElement().attribute("name");
|
||||
|
||||
// Two terminals with one name cannot be told apart in a wiring list
|
||||
// (IEC 61666), the same rule the element editor applies on save.
|
||||
const auto repeated = TerminalNameCheck::repeatedNames(terminal_names);
|
||||
if (!repeated.isEmpty()) {
|
||||
out << "FAIL " << path << " (repeated terminal names: "
|
||||
<< TerminalNameCheck::describe(repeated) << ")\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Negative dimensions are malformed but QET still loads them; surface as a
|
||||
// warning rather than a failure so this agrees with QET's own loader.
|
||||
@@ -528,6 +542,15 @@ int checkOneElement(const QString &path)
|
||||
return 1;
|
||||
}
|
||||
|
||||
const QString type = root.attribute("link_type");
|
||||
const int unnamed = TerminalNameCheck::unnamedCount(terminal_names);
|
||||
if (unnamed && !type.endsWith("_report")
|
||||
&& type != "conductor_definition" && type != "thumbnail") {
|
||||
out << "WARN " << path << " (" << unnamed << " of " << terminals
|
||||
<< " terminals have no name)\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
out << "OK " << path << " (" << terminals << " terminals)\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
#ifndef TERMINALNAMECHECK_H
|
||||
#define TERMINALNAMECHECK_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
The terminal name check run when an element is saved, and by
|
||||
--check-elements. IEC 61666 requires every terminal to be identified
|
||||
unambiguously within its object: two terminals of one element must not
|
||||
share a name, and a terminal should have one.
|
||||
|
||||
Names are compared after trimming spaces at both ends, and are case
|
||||
sensitive ("n" and "N" are different names).
|
||||
*/
|
||||
namespace TerminalNameCheck
|
||||
{
|
||||
/// QSettings key: false turns the check off in the element editor.
|
||||
inline const QString settings_key{QStringLiteral("elementeditor/check-terminal-names")};
|
||||
|
||||
/// The names used by more than one terminal, with how many terminals
|
||||
/// use each, in the order the names first appear. Unnamed terminals
|
||||
/// are not counted here.
|
||||
inline QList<QPair<QString, int>> repeatedNames(const QStringList &names)
|
||||
{
|
||||
QHash<QString, int> count;
|
||||
QStringList order;
|
||||
for (const QString &raw : names) {
|
||||
const QString name = raw.trimmed();
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!count.contains(name)) {
|
||||
order << name;
|
||||
}
|
||||
++count[name];
|
||||
}
|
||||
|
||||
QList<QPair<QString, int>> repeated;
|
||||
for (const QString &name : order) {
|
||||
if (count.value(name) > 1) {
|
||||
repeated << qMakePair(name, count.value(name));
|
||||
}
|
||||
}
|
||||
return repeated;
|
||||
}
|
||||
|
||||
/// The number of terminals without a name.
|
||||
inline int unnamedCount(const QStringList &names)
|
||||
{
|
||||
int unnamed = 0;
|
||||
for (const QString &name : names) {
|
||||
if (name.trimmed().isEmpty()) {
|
||||
++unnamed;
|
||||
}
|
||||
}
|
||||
return unnamed;
|
||||
}
|
||||
|
||||
/// "N ×3, L ×2": the repeated names, for messages.
|
||||
inline QString describe(const QList<QPair<QString, int>> &repeated)
|
||||
{
|
||||
QStringList parts;
|
||||
for (const auto &entry : repeated) {
|
||||
parts << QStringLiteral("%1 ×%2").arg(entry.first, QString::number(entry.second));
|
||||
}
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TERMINALNAMECHECK_H
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "../esevent/eseventadddynamictextfield.h"
|
||||
#include "../../elementdialog.h"
|
||||
#include "../graphicspart/partterminal.h"
|
||||
#include "../terminalnamecheck.h"
|
||||
#include "../arceditor.h"
|
||||
#include "ellipseeditor.h"
|
||||
#include "lineeditor.h"
|
||||
@@ -839,6 +840,53 @@ bool QETElementEditor::checkElement()
|
||||
}
|
||||
}
|
||||
|
||||
// Check terminal names: repeated names are an error, missing names a warning
|
||||
if (QSettings().value(TerminalNameCheck::settings_key, true).toBool())
|
||||
{
|
||||
QList<PartTerminal *> terminals;
|
||||
QStringList names;
|
||||
for (auto qgi : m_elmt_scene -> items()) {
|
||||
if (auto terminal = qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminals << terminal;
|
||||
names << terminal -> terminalName();
|
||||
}
|
||||
}
|
||||
|
||||
const auto repeated = TerminalNameCheck::repeatedNames(names);
|
||||
if (!repeated.isEmpty())
|
||||
{
|
||||
errors << qMakePair (tr("Noms de bornes en double"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Plusieurs bornes portent le même nom : %1."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Donner un nom unique à chaque borne, par exemple N.1 et N.2."
|
||||
" Les bornes concernées sont sélectionnées.")
|
||||
.arg(TerminalNameCheck::describe(repeated).toHtmlEscaped()));
|
||||
|
||||
m_elmt_scene -> clearSelection();
|
||||
for (auto terminal : terminals) {
|
||||
for (const auto &entry : repeated) {
|
||||
if (terminal -> terminalName().trimmed() == entry.first) {
|
||||
terminal -> setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int unnamed = TerminalNameCheck::unnamedCount(names);
|
||||
if (unnamed &&
|
||||
!(m_elmt_scene->elementData().m_type & ElementData::AllReport) &&
|
||||
m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition &&
|
||||
m_elmt_scene->elementData().m_type != ElementData::Thumbnail)
|
||||
{
|
||||
warnings << qMakePair (tr("Bornes sans nom"),
|
||||
tr("<br>%n borne(s) sans nom. Sans noms de bornes uniques,"
|
||||
" la liste de câblage (qui relie quoi à quoi) ne peut pas"
|
||||
" désigner chaque borne, et ne peut donc pas servir à"
|
||||
" câbler l'armoire en atelier.", "", unnamed));
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.count() && !warnings.count()) {
|
||||
return(true);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "../../utils/qetutils.h"
|
||||
#include "../../qetmessagebox.h"
|
||||
#include "../../textgrid.h"
|
||||
#include "../../editor/terminalnamecheck.h"
|
||||
#include "../nokde/kcolorbutton.h"
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
@@ -174,6 +175,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->MaxPartsElementEditorList_sb->setValue(settings.value("elementeditor/max-parts-element-editor-list", 200).toInt());
|
||||
ui->ElementEditor_Grid_PointSize_min_sb->setValue(settings.value("elementeditor/grid_pointsize_min", 1).toInt());
|
||||
ui->ElementEditor_Grid_PointSize_max_sb->setValue(settings.value("elementeditor/grid_pointsize_max", 1).toInt());
|
||||
ui->m_check_terminal_names_cb->setChecked(settings.value(TerminalNameCheck::settings_key, true).toBool());
|
||||
|
||||
QString path = settings.value("elements-collections/common-collection-path", "default").toString();
|
||||
if (path != "default")
|
||||
@@ -286,6 +288,7 @@ void GeneralConfigurationPage::applyConf()
|
||||
settings.setValue("elementeditor/max-parts-element-editor-list", ui->MaxPartsElementEditorList_sb->value());
|
||||
settings.setValue("elementeditor/grid_pointsize_min", ui->ElementEditor_Grid_PointSize_min_sb->value());
|
||||
settings.setValue("elementeditor/grid_pointsize_max", ui->ElementEditor_Grid_PointSize_max_sb->value());
|
||||
settings.setValue(TerminalNameCheck::settings_key, ui->m_check_terminal_names_cb->isChecked());
|
||||
|
||||
//DIAGRAM VIEW
|
||||
settings.setValue("diagramview/gestures", ui->m_use_gesture_trackpad->isChecked());
|
||||
|
||||
@@ -1240,6 +1240,19 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_check_terminal_names_cb">
|
||||
<property name="toolTip">
|
||||
<string>À l'enregistrement d'un élément : refuser deux bornes portant le même nom, et signaler les bornes sans nom (IEC 61666).</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Vérifier les noms des bornes à l'enregistrement</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_6">
|
||||
<property name="orientation">
|
||||
@@ -1275,6 +1288,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
<tabstop>m_custom_tbt_path_cb</tabstop>
|
||||
<tabstop>m_highlight_integrated_elements</tabstop>
|
||||
<tabstop>m_default_elements_info</tabstop>
|
||||
<tabstop>m_check_terminal_names_cb</tabstop>
|
||||
<tabstop>m_lang_cb</tabstop>
|
||||
<tabstop>m_dyn_text_font_pb</tabstop>
|
||||
<tabstop>m_dyn_text_rotation_sb</tabstop>
|
||||
|
||||
@@ -111,6 +111,13 @@ add_test(NAME tst_alignment COMMAND tst_alignment)
|
||||
target_include_directories(tst_alignment PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_alignment PRIVATE Qt::Test)
|
||||
|
||||
# terminalnamecheck.h is header-only: the terminal name rule applied by the
|
||||
# element editor on save and by --check-elements.
|
||||
add_executable(tst_terminalnamecheck tst_terminalnamecheck.cpp)
|
||||
add_test(NAME tst_terminalnamecheck COMMAND tst_terminalnamecheck)
|
||||
target_include_directories(tst_terminalnamecheck PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_terminalnamecheck PRIVATE Qt::Test)
|
||||
|
||||
# configprofile.h is header-only: which settings a saved configuration
|
||||
# profile carries, and how loading one replaces the live settings.
|
||||
add_executable(tst_configprofile tst_configprofile.cpp)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <QtTest>
|
||||
|
||||
#include "editor/terminalnamecheck.h"
|
||||
|
||||
class tst_terminalnamecheck : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void uniqueNamesPass()
|
||||
{
|
||||
QVERIFY(TerminalNameCheck::repeatedNames({"A1", "A2", "13", "14"}).isEmpty());
|
||||
}
|
||||
|
||||
// Shelly Pro 2PM in the shipped collection: three terminals named N.
|
||||
void repeatedNamesAreCounted()
|
||||
{
|
||||
const auto repeated = TerminalNameCheck::repeatedNames(
|
||||
{"L", "N", "O1", "N", "O2", "N", "L"});
|
||||
QCOMPARE(repeated.size(), 2);
|
||||
QCOMPARE(repeated.at(0), qMakePair(QString("L"), 2));
|
||||
QCOMPARE(repeated.at(1), qMakePair(QString("N"), 3));
|
||||
QCOMPARE(TerminalNameCheck::describe(repeated),
|
||||
QString::fromUtf8("L ×2, N ×3"));
|
||||
}
|
||||
|
||||
void surroundingSpacesAreIgnored()
|
||||
{
|
||||
QCOMPARE(TerminalNameCheck::repeatedNames({"PE", " PE "}).size(), 1);
|
||||
}
|
||||
|
||||
void caseMatters()
|
||||
{
|
||||
QVERIFY(TerminalNameCheck::repeatedNames({"n", "N"}).isEmpty());
|
||||
}
|
||||
|
||||
// Unnamed terminals are reported as unnamed, never as a repeat of "".
|
||||
void unnamedAreNotRepeats()
|
||||
{
|
||||
const QStringList names{"", " ", "1", ""};
|
||||
QVERIFY(TerminalNameCheck::repeatedNames(names).isEmpty());
|
||||
QCOMPARE(TerminalNameCheck::unnamedCount(names), 3);
|
||||
}
|
||||
|
||||
// describe() goes into a tr() message with %1: a name holding %2 stays literal.
|
||||
void describeKeepsPercentLiteral()
|
||||
{
|
||||
QCOMPARE(TerminalNameCheck::describe({qMakePair(QString("%2"), 2)}),
|
||||
QString::fromUtf8("%2 ×2"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_terminalnamecheck)
|
||||
|
||||
#include "tst_terminalnamecheck.moc"
|
||||
Reference in New Issue
Block a user