mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 22:54:12 +02:00
Merge pull request #1438 from Kellermorph/wiring-manager
Add cable management: drawing, cores, numbering, cross-references and cable list
This commit is contained in:
@@ -87,6 +87,15 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/attached_plain_paste_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_attachedplainpaste PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
# Deleting a cable, undoing the delete twice and then editing anything
|
||||
# has both cable commands tearing down the same cable. Runs under the
|
||||
# same objects the application uses, so a second free is caught.
|
||||
add_test(NAME tst_cableundointegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_cable_undo_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/cable_undo_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_cableundointegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME tst_darkimageintegration
|
||||
@@ -254,6 +263,23 @@ add_test(NAME tst_legacyelementtypes COMMAND tst_legacyelementtypes)
|
||||
target_include_directories(tst_legacyelementtypes PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_legacyelementtypes PRIVATE Qt::Test)
|
||||
|
||||
# The cable type catalogue is plain data (it reads and writes the CSV file
|
||||
# holding the cable kinds), so it is compiled here on its own, together with
|
||||
# the material file helpers it shares and the element information wording.
|
||||
add_executable(
|
||||
tst_cabletypelist
|
||||
tst_cabletypelist.cpp
|
||||
${QET_DIR}/sources/cablelist/cabletypelist.cpp
|
||||
${QET_DIR}/sources/cablelist/cabletypelist.h
|
||||
${QET_DIR}/sources/materiallist/materiallist.cpp
|
||||
${QET_DIR}/sources/materiallist/materiallist.h
|
||||
${QET_DIR}/sources/qetinformation.cpp
|
||||
${QET_DIR}/sources/qetinformation.h)
|
||||
add_test(NAME tst_cabletypelist COMMAND tst_cabletypelist)
|
||||
target_include_directories(tst_cabletypelist PRIVATE ${QET_DIR}/sources)
|
||||
# qetinformation.h uses QRegularExpressionValidator, which lives in QtGui.
|
||||
target_link_libraries(tst_cabletypelist PRIVATE Qt::Test Qt::Gui)
|
||||
|
||||
add_executable(
|
||||
tst_qetpalette
|
||||
tst_qetpalette.cpp
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Cable undo ownership regression, linked against the application's objects.
|
||||
//
|
||||
// Deleting a cable and undoing the delete twice leaves the cable out of
|
||||
// the project while AddCableCommand still holds it, and the next edit
|
||||
// makes the stack throw both commands away. Whoever of the two runs
|
||||
// first frees the cable; the other one must not follow it into the freed
|
||||
// memory. A raw pointer there meant freeing the same cable twice, which
|
||||
// a normal build hides and AddressSanitizer reports as a heap
|
||||
// use-after-free in ~AddCableCommand.
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../../sources/diagram.h"
|
||||
#include "../../sources/qetproject.h"
|
||||
#include "../../sources/qetmessagebox.h"
|
||||
#include "../../sources/cable/addcablecommand.h"
|
||||
#include "../../sources/cable/cable.h"
|
||||
#include "../../sources/cable/cablepart.h"
|
||||
#include "../../sources/cable/editcablecommand.h"
|
||||
|
||||
static void check(bool value, const char *message)
|
||||
{
|
||||
if (!value) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
// Any edit the user makes afterwards: pushing it is what makes the stack
|
||||
// discard the commands it still holds undone, and that is where the two
|
||||
// destructors meet the same cable.
|
||||
struct AnyEdit : QUndoCommand
|
||||
{
|
||||
void redo() override {}
|
||||
void undo() override {}
|
||||
};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
std::freopen(argv[2], "w", stdout);
|
||||
|
||||
QTemporaryDir settings;
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path());
|
||||
QCoreApplication::setOrganizationName("QETCableUndoRegression");
|
||||
QETProject::setBackupEnabled(false);
|
||||
QET::QetMessageBox::setNonInteractive(true);
|
||||
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
|
||||
|
||||
try
|
||||
{
|
||||
QETProject project(QString::fromLocal8Bit(argv[1]));
|
||||
check(project.state() == QETProject::Ok, "fixture opens");
|
||||
Diagram *scene = project.diagrams().first();
|
||||
|
||||
// The state the cable tool leaves behind when it has drawn one
|
||||
// line and the dialog accepted it: a cable holding the section,
|
||||
// a line on the folio pointing back at that cable.
|
||||
CablePartData data;
|
||||
data.uuid = QUuid::createUuid();
|
||||
data.diagram = scene->uuid();
|
||||
data.p1 = QPointF(100, 100);
|
||||
data.p2 = QPointF(300, 100);
|
||||
|
||||
//The cable the create dialog hands over: owned by the project,
|
||||
//which is the only way it may enter the project's list at all.
|
||||
auto *cable = project.newCable();
|
||||
cable->addPart(data);
|
||||
auto *item = new CablePart(data);
|
||||
item->setCable(cable);
|
||||
scene->addItem(item);
|
||||
|
||||
// Draw the line.
|
||||
scene->undoStack().push(new AddCableCommand(cable, item, scene, true));
|
||||
check(project.cables().contains(cable), "drawn cable is in the project");
|
||||
|
||||
// Select all and delete.
|
||||
scene->undoStack().push(new RemoveCableCommand(scene, {item}));
|
||||
check(!project.cables().contains(cable), "delete took the cable out of the project");
|
||||
|
||||
// Ctrl+Z, Ctrl+Z: the cable is in nobody's hands now, but both
|
||||
// commands still stand and both think they may have to clean it up.
|
||||
scene->undoStack().undo();
|
||||
scene->undoStack().undo();
|
||||
check(!project.cables().contains(cable), "cable orphaned by two undos");
|
||||
|
||||
// The next edit of any kind: the stack throws the undone commands
|
||||
// away, and ~RemoveCableCommand frees the cable before
|
||||
// ~AddCableCommand gets to look at it.
|
||||
scene->undoStack().push(new AnyEdit);
|
||||
|
||||
check(project.state() == QETProject::Ok, "project survives the edit");
|
||||
std::puts("PASS: deleting a cable, undoing twice and editing again frees it exactly once");
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
std::printf("FAIL: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the cable undo integration probe using an existing Ninja app build.
|
||||
|
||||
Reuses app objects without duplicating the app or introducing a core-library
|
||||
refactor. No application source or existing executable is replaced. The
|
||||
generated manifest, probe binary, reports and export fixtures stay in build/.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', required=True)
|
||||
parser.add_argument('--binary', required=True)
|
||||
parser.add_argument('--source', required=True)
|
||||
parser.add_argument('--fixture', required=True)
|
||||
args = parser.parse_args()
|
||||
build = pathlib.Path(args.build).resolve()
|
||||
binary = pathlib.Path(args.binary).resolve()
|
||||
source = pathlib.Path(args.source).resolve()
|
||||
fixture = pathlib.Path(args.fixture).resolve()
|
||||
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
|
||||
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
|
||||
if not match:
|
||||
sys.exit('Cannot find the application main object in the Ninja build.')
|
||||
main_object = match.group(1)
|
||||
probe_object = main_object.replace('main.cpp.', 'cable_undo_probe.cpp.')
|
||||
probe_binary = binary.with_name('cable_undo_probe' + binary.suffix)
|
||||
def escape(path):
|
||||
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
|
||||
main_source = source.parents[2] / 'sources' / 'main.cpp'
|
||||
manifest = manifest.replace(main_object, probe_object)
|
||||
manifest = manifest.replace(escape(main_source), escape(source))
|
||||
if binary.suffix:
|
||||
manifest = manifest.replace(binary.name, probe_binary.name)
|
||||
else:
|
||||
old_target = escape(binary.name if binary.parent == build else binary)
|
||||
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
|
||||
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
|
||||
old_file = binary.name if binary.parent == build else binary.as_posix()
|
||||
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
|
||||
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
|
||||
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
|
||||
probe_manifest = build / 'cable-undo-probe.ninja'
|
||||
probe_manifest.write_text(manifest, encoding='utf-8')
|
||||
ninja = shutil.which('ninja')
|
||||
if not ninja:
|
||||
sys.exit('Ninja is required for the integration probe.')
|
||||
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
|
||||
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
|
||||
output = build / 'cable-undo'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
#include "cablelist/cabletypelist.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
/**
|
||||
The cable type catalogue is read back the way it was written, and the
|
||||
three columns it really uses are the only ones a new file gets:
|
||||
|
||||
- a catalogue carrying the material file's whole shape (ten columns,
|
||||
two header lines) reads as ten columns, its machine header line is
|
||||
not mistaken for a cable, and the type comes out of it : this is
|
||||
the catalogue a user already has on disk;
|
||||
- a file the program creates itself holds the two header lines and
|
||||
the three columns, no more;
|
||||
- a file written by hand with the label line alone still gives the
|
||||
type, so dropping the machine header never costs a column;
|
||||
- and a line of cables is never taken for the machine header, even
|
||||
when it happens to be written in key shaped words.
|
||||
*/
|
||||
class tst_cabletypelist : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QString write(const QString &name, const QString &content)
|
||||
{
|
||||
const QString path = m_dir.filePath(name);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
return {};
|
||||
}
|
||||
file.write(content.toUtf8());
|
||||
file.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
bool load(const QString &path, CableTypeListData *data)
|
||||
{
|
||||
QString error;
|
||||
const bool ok = CableTypeList::load(path, data, &error);
|
||||
if (!ok) {
|
||||
qWarning("%s", qPrintable(error));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void tenColumnCatalogueReadsBack()
|
||||
{
|
||||
const QString path = write(QStringLiteral("ten.csv"),
|
||||
QStringLiteral(
|
||||
"Désignation;Nombre d'âmes;Couleurs des âmes;Section du câble;Couleur du fourreau;Fabricant;Numéro de commande;Fournisseur;Description textuelle;Notes\n"
|
||||
"designation;cores;core_colors;section;color;manufacturer;manufacturer_reference;supplier;description;notes\n"
|
||||
"H07V-K 3G1,5;3;wh|bn|bl;1.5;PVC;Nexans;X123;Rexel;courant fort;note\n"
|
||||
"NYM-J 3G2,5;3;wh|bn|bl;2.5;PVC;;;;;\n"));
|
||||
QVERIFY(!path.isEmpty());
|
||||
|
||||
CableTypeListData data;
|
||||
QVERIFY2(load(path, &data), "the catalogue could not be read");
|
||||
|
||||
//The machine header line is not one of the cables...
|
||||
QCOMPARE(data.records.size(), 2);
|
||||
//... and every column it names is read under that name, even the
|
||||
//ones this program never gave a meaning to.
|
||||
QCOMPARE(data.columns, QStringList({
|
||||
QStringLiteral("designation"), QStringLiteral("cores"),
|
||||
QStringLiteral("core_colors"), QStringLiteral("section"),
|
||||
QStringLiteral("color"), QStringLiteral("manufacturer"),
|
||||
QStringLiteral("manufacturer_reference"),
|
||||
QStringLiteral("supplier"), QStringLiteral("description"),
|
||||
QStringLiteral("notes")}));
|
||||
|
||||
QCOMPARE(CableTypeList::designation(data.records.at(0)),
|
||||
QStringLiteral("H07V-K 3G1,5"));
|
||||
QCOMPARE(CableTypeList::coreCount(data.records.at(0)), 3);
|
||||
QCOMPARE(CableTypeList::coreColors(data.records.at(0)),
|
||||
QStringList({QStringLiteral("wh"), QStringLiteral("bn"),
|
||||
QStringLiteral("bl")}));
|
||||
//A column outside the three the program uses keeps its name and
|
||||
//its data.
|
||||
QCOMPARE(data.records.at(0).value(QStringLiteral("section")),
|
||||
QStringLiteral("1.5"));
|
||||
QCOMPARE(CableTypeList::designation(data.records.at(1)),
|
||||
QStringLiteral("NYM-J 3G2,5"));
|
||||
}
|
||||
|
||||
void newFileHasThreeColumns()
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("new.csv"));
|
||||
QVERIFY2(CableTypeList::createFile(path), "the file was not created");
|
||||
|
||||
CableTypeListData data;
|
||||
QVERIFY(load(path, &data));
|
||||
QCOMPARE(data.columns, QStringList({
|
||||
QStringLiteral("designation"), QStringLiteral("cores"),
|
||||
QStringLiteral("core_colors")}));
|
||||
QVERIFY(data.records.isEmpty());
|
||||
//The label line reads the way the cable form names the type.
|
||||
QCOMPARE(CableTypeList::translatedHeader(data.columns), QStringList({
|
||||
QStringLiteral("Designation"),
|
||||
QStringLiteral("Number of cores"),
|
||||
QStringLiteral("Core colors")}));
|
||||
|
||||
CableTypeRecord record;
|
||||
record.setValue(QStringLiteral("designation"),
|
||||
QStringLiteral("H07V-K 3G1,5"));
|
||||
record.setValue(QStringLiteral("cores"), QStringLiteral("3"));
|
||||
record.setValue(QStringLiteral("core_colors"),
|
||||
QStringLiteral("wh|bn|bl"));
|
||||
QVERIFY2(CableTypeList::appendRecord(path, record),
|
||||
"the record was not appended");
|
||||
|
||||
CableTypeListData read_back;
|
||||
QVERIFY(load(path, &read_back));
|
||||
QCOMPARE(read_back.columns, data.columns);
|
||||
QCOMPARE(read_back.records.size(), 1);
|
||||
QCOMPARE(CableTypeList::designation(read_back.records.first()),
|
||||
QStringLiteral("H07V-K 3G1,5"));
|
||||
QCOMPARE(CableTypeList::coreCount(read_back.records.first()), 3);
|
||||
QCOMPARE(CableTypeList::coreColors(read_back.records.first()),
|
||||
QStringList({QStringLiteral("wh"), QStringLiteral("bn"),
|
||||
QStringLiteral("bl")}));
|
||||
}
|
||||
|
||||
//The folder a cable type file goes into may not exist yet: a fresh
|
||||
//profile has no documents folder, and a path typed into the settings
|
||||
//may name one which was never created. The write makes the folder
|
||||
//rather than failing with a message which says nothing about why.
|
||||
void aMissingFolderIsCreated()
|
||||
{
|
||||
const QString path = m_dir.filePath(
|
||||
QStringLiteral("not/there/yet/qet_cable_types.csv"));
|
||||
QVERIFY(!QFile::exists(path));
|
||||
|
||||
QString error;
|
||||
QVERIFY2(CableTypeList::createFile(path, &error), qPrintable(error));
|
||||
QVERIFY2(QFile::exists(path), "the file was not created");
|
||||
|
||||
CableTypeListData data;
|
||||
QVERIFY2(load(path, &data), "the file just created could not be read");
|
||||
QVERIFY(data.records.isEmpty());
|
||||
}
|
||||
|
||||
void labelLineAloneStillGivesTheType()
|
||||
{
|
||||
const QString path = write(QStringLiteral("hand.csv"),
|
||||
QStringLiteral(
|
||||
"Désignation;Nombre d'âmes;Couleurs des âmes\n"
|
||||
"H07V-K 3G1,5;3;wh|bn|bl\n"));
|
||||
QVERIFY(!path.isEmpty());
|
||||
|
||||
CableTypeListData data;
|
||||
QVERIFY2(load(path, &data), "the hand written file could not be read");
|
||||
QCOMPARE(data.columns, QStringList({
|
||||
QStringLiteral("designation"), QStringLiteral("cores"),
|
||||
QStringLiteral("core_colors")}));
|
||||
QCOMPARE(data.records.size(), 1);
|
||||
QCOMPARE(CableTypeList::designation(data.records.first()),
|
||||
QStringLiteral("H07V-K 3G1,5"));
|
||||
QCOMPARE(CableTypeList::coreCount(data.records.first()), 3);
|
||||
}
|
||||
|
||||
void aCableIsNeverMistakenForTheHeader()
|
||||
{
|
||||
//Three cells which all look like column keys, none of which means
|
||||
//anything here : far more likely to be a cable than a header.
|
||||
const QString path = write(QStringLiteral("unknown.csv"),
|
||||
QStringLiteral(
|
||||
"Désignation;Nombre d'âmes;Couleurs des âmes\n"
|
||||
"ymj;;wh\n"
|
||||
"H07V-K 3G1,5;3;wh|bn|bl\n"));
|
||||
QVERIFY(!path.isEmpty());
|
||||
|
||||
CableTypeListData data;
|
||||
QVERIFY2(load(path, &data), "the file could not be read");
|
||||
QCOMPARE(data.records.size(), 2);
|
||||
QCOMPARE(CableTypeList::designation(data.records.first()),
|
||||
QStringLiteral("ymj"));
|
||||
QCOMPARE(CableTypeList::designation(data.records.last()),
|
||||
QStringLiteral("H07V-K 3G1,5"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_cabletypelist)
|
||||
#include "tst_cabletypelist.moc"
|
||||
Reference in New Issue
Block a user