Address smart device BOM review feedback

This commit is contained in:
enesgursoy6110
2026-09-10 20:02:42 +03:00
parent 16dbc5c404
commit 77f8e26ffc
20 changed files with 397 additions and 880 deletions
+109 -299
View File
@@ -15,311 +15,121 @@
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QTemporaryDir>
#include <QSettings>
#include <QSqlError>
#include <algorithm>
#include "bomexport.h"
#include "ElementsCollection/xmlelementcollection.h"
#include "dataBase/projectdatabase.h"
#include "devicebomexport.h"
#include "diagram.h"
#include "qetgraphicsitem/element.h"
#include "qetinformation.h"
#include "qetproject.h"
#include "qetresult.h"
#include "ui/elementinfopartwidget.h"
#include "ui/elementinfowidget.h"
#include <QFile>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QTemporaryDir>
#include <QtTest>
class SmartDeviceTest : public QObject
{
Q_OBJECT
QTemporaryDir m_settings;
Element *addElement(QETProject &project, Diagram *diagram, const QString &tag,
ElementData::Type type = ElementData::Simple)
{
QDomDocument doc;
doc.setContent(QStringLiteral(
"<definition version=\"0.100.0\" type=\"element\" link_type=\"simple\" "
"width=\"20\" height=\"20\" hotspot_x=\"0\" hotspot_y=\"0\">"
"<names><name lang=\"en\">Test device</name></names>"
"<description><rect x=\"0\" y=\"0\" width=\"20\" height=\"20\" "
"antialias=\"false\" style=\"line-style:normal;line-weight:normal;"
"filling:none;color:black\"/></description></definition>"));
const QString name = QUuid::createUuid().toString(QUuid::WithoutBraces) + ".elmt";
if (!project.embeddedElementCollection()->addElementDefinition("import", name, doc.documentElement()))
return nullptr;
int state = -1;
auto *element = new Element(ElementsLocation("embed://import/" + name, &project), nullptr, &state);
if (state != 0) {
delete element;
return nullptr;
}
diagram->addItem(element);
auto data = element->elementData();
data.m_type = type;
data.m_informations.addValue("label", tag);
element->setElementData(data);
return element;
}
DeviceInformation sample() const
{
DeviceInformation info;
info.m_device_tag = "-K1";
info.m_manufacturer = QString::fromUtf8("\xC3\x96rnek Elektrik");
info.m_part_number = "001234";
info.m_model = "RX-24";
info.m_description = QString::fromUtf8("G\xC3\xBC\xC3\xA7 kontakt\xC3\xB6r\xC3\xBC");
info.m_category = "Contactor";
info.m_quantity = "2.5";
info.m_voltage_rating = "24 V DC";
info.m_current_rating = "16 A";
info.m_notes = "Panel A, \"spare\"; check\nSecond line";
return info;
}
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("QET-SmartDeviceTests");
QCoreApplication::setApplicationName("IsolatedTests");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, m_settings.path());
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, m_settings.path());
QETProject::setBackupEnabled(false);
}
void emptyAndLegacyContext()
{
const auto empty = DeviceInformation::fromContext(DiagramContext());
QVERIFY(empty.toContext().keys().isEmpty());
QVERIFY(empty.m_quantity.isEmpty());
QDomDocument doc;
QVERIFY(doc.setContent(QStringLiteral(
"<elementInformations><elementInformation name=\"label\">K7</elementInformation>"
"<elementInformation name=\"manufacturer_reference\">0007</elementInformation>"
"</elementInformations>")));
DiagramContext context;
context.fromXml(doc.documentElement(), "elementInformation");
const auto legacy = DeviceInformation::fromContext(context);
QCOMPARE(legacy.m_device_tag, QString("K7"));
QCOMPARE(legacy.m_part_number, QString("0007"));
QVERIFY(legacy.m_model.isEmpty());
QVERIFY(legacy.m_voltage_rating.isEmpty());
QVERIFY(legacy.m_current_rating.isEmpty());
QVERIFY(legacy.m_notes.isEmpty());
}
void contextRoundTripAndMerge()
{
DiagramContext original;
original.addValue("custom_value", "Keep", false);
original.addValue("manufacturer", "Old", false);
original.addValue("exclude_from_bom", "true");
auto info = sample();
auto context = info.toContext(original);
QVERIFY(!context.keyMustShow("manufacturer"));
QVERIFY(!context.keyMustShow("custom_value"));
QCOMPARE(context.value("custom_value").toString(), QString("Keep"));
QDomDocument doc;
auto xml = doc.createElement("elementInformations");
doc.appendChild(xml);
context.toXml(xml, "elementInformation");
QDomDocument reparsed;
QVERIFY(reparsed.setContent(doc.toByteArray()));
DiagramContext restored;
restored.fromXml(reparsed.documentElement(), "elementInformation");
QVERIFY(restored == context);
QVERIFY(DeviceInformation::fromContext(restored).toContext() == info.toContext());
info.m_model.clear();
const auto cleared = info.toContext(restored);
QVERIFY(!cleared.contains("model"));
QCOMPARE(cleared.value("exclude_from_bom").toString(), QString("true"));
}
void projectSaveLoad()
{
QTemporaryDir temp;
QETProject project;
auto *diagram = project.addNewDiagram();
diagram->border_and_titleblock.setFolio("A-01");
auto *element = addElement(project, diagram, "-K1");
QVERIFY(element);
element->setElementInformations(sample().toContext());
auto *second_diagram = project.addNewDiagram();
second_diagram->border_and_titleblock.setFolio("A-02");
diagram->border_and_titleblock.setFolioData(1, 2);
second_diagram->border_and_titleblock.setFolioData(2, 2);
auto *legacy = addElement(project, second_diagram, "-K2");
QVERIFY(legacy);
const auto before = DeviceBomExport::toCsv(DeviceBomExport::collect(project));
project.setFilePath(temp.filePath("roundtrip.qet"));
const auto result = project.write();
QVERIFY2(result.isOk(), qPrintable(result.errorMessage()));
QETProject restored(project.filePath());
QCOMPARE(restored.state(), QETProject::Ok);
QCOMPARE(restored.diagrams().size(), 2);
const auto rows = DeviceBomExport::collect(restored);
QCOMPARE(rows.size(), 2);
QCOMPARE(rows.first().m_page, QString("A-01"));
QCOMPARE(rows.last().m_page, QString("A-02"));
QVERIFY(rows.first().m_device.toContext() == sample().toContext());
QVERIFY(rows.last().m_device.m_model.isEmpty());
QVERIFY(rows.last().m_device.m_quantity.isEmpty());
QCOMPARE(DeviceBomExport::toCsv(rows), before);
const QString artifacts = qEnvironmentVariable("QET_SMART_DEVICE_ARTIFACT_DIR");
if (!artifacts.isEmpty()) {
QDir().mkpath(artifacts);
restored.setFilePath(QDir(artifacts).filePath("sample.qet"));
QVERIFY(restored.write().isOk());
QVERIFY(DeviceBomExport::writeCsv(restored, QDir(artifacts).filePath("sample.csv")));
}
}
void collectDevices()
{
QETProject project;
auto *first = project.addNewDiagram();
auto *second = project.addNewDiagram();
first->border_and_titleblock.setFolio("DEV-01");
second->border_and_titleblock.setFolio("DEV-10");
first->border_and_titleblock.setFolioData(1, 2);
second->border_and_titleblock.setFolioData(2, 2);
for (auto type : {ElementData::Simple, ElementData::Master, ElementData::Terminal,
ElementData::Slave, ElementData::NextReport, ElementData::PreviousReport,
ElementData::Thumbnail, ElementData::ConductorDefinition}) {
auto *element = addElement(project, first, QString::number(type), type);
QVERIFY(element);
if (type == ElementData::Master) {
auto data = element->elementData();
data.m_master_type = ElementData::PLC;
data.m_plc_master_data.ios.resize(2);
element->setElementData(data);
}
}
auto *excluded = addElement(project, first, "Excluded");
QVERIFY(excluded);
auto context = excluded->elementInformations();
context.addValue("exclude_from_bom", "true");
excluded->setElementInformations(context);
for (const QString tag : {"Z2", "A2"}) {
auto *element = addElement(project, second, tag);
QVERIFY(element);
auto info = sample();
info.m_device_tag = tag;
element->setElementInformations(info.toContext());
}
const auto rows = DeviceBomExport::collect(project);
QCOMPARE(rows.size(), 4);
QCOMPARE(rows.at(0).m_page, QString("DEV-01"));
QCOMPARE(rows.at(0).m_device.m_device_tag, QString("1"));
QCOMPARE(rows.at(1).m_device.m_device_tag, QString("8"));
QCOMPARE(rows.at(2).m_page, QString("DEV-10"));
QCOMPARE(rows.at(2).m_device.m_device_tag, QString("A2"));
QCOMPARE(rows.at(3).m_device.m_device_tag, QString("Z2"));
QCOMPARE(rows.at(2).m_device.m_part_number, rows.at(3).m_device.m_part_number);
QCOMPARE(rows.at(2).m_device.m_quantity, QString("2.5"));
QCOMPARE(DeviceBomExport::toCsv(rows), DeviceBomExport::toCsv(DeviceBomExport::collect(project)));
}
void emptyFolioFallsBackToPagePosition()
{
QETProject project;
auto *diagram = project.addNewDiagram();
diagram->border_and_titleblock.setFolio(QString());
diagram->border_and_titleblock.setFolioData(1, 1);
QVERIFY(addElement(project, diagram, "K1"));
const auto rows = DeviceBomExport::collect(project);
QCOMPARE(rows.size(), 1);
QCOMPARE(rows.first().m_page, QString("1"));
}
void csvEscapingAndEncoding()
{
const auto csv = DeviceBomExport::toCsv({{sample(), "F-03"}});
const QByteArray header("\xEF\xBB\xBF\"No\",\"Device tag\",\"Manufacturer\",\"Part number\","
"\"Model\",\"Description\",\"Category\",\"Quantity\",\"Page\",\"Notes\"\r\n");
QVERIFY(csv.startsWith(header));
QVERIFY(csv.contains(sample().m_manufacturer.toUtf8()));
QVERIFY(csv.contains("\"001234\""));
QVERIFY(!csv.contains("24 V DC"));
QVERIFY(!csv.contains("16 A"));
QVERIFY(csv.endsWith("\"2.5\",\"F-03\",\"Panel A, \"\"spare\"\"; check\nSecond line\"\r\n"));
QCOMPARE(DeviceBomExport::toCsv({}), header);
}
void fileSuccessAndFailure()
{
QTemporaryDir temp;
QETProject project;
QString error = "old error";
QVERIFY(!DeviceBomExport::writeCsv(project, temp.filePath("absent/bom.csv"), &error));
QVERIFY(!error.isEmpty());
const auto path = temp.filePath("bom.csv");
QVERIFY(DeviceBomExport::writeCsv(project, path, &error));
QVERIFY(error.isEmpty());
QVERIFY(DeviceBomExport::writeCsv(project, path, &error));
QFile file(path);
QVERIFY(file.open(QIODevice::ReadOnly));
QCOMPARE(file.readAll(), DeviceBomExport::toCsv({}));
}
void propertyEditorUndoRedo()
{
QETProject project;
auto *diagram = project.addNewDiagram();
auto *element = addElement(project, diagram, "-K1");
QVERIFY(element);
ElementInfoWidget editor(element);
const auto context = sample().toContext();
const auto widgets = editor.findChildren<ElementInfoPartWidget *>();
for (const QString &key : context.keys()) {
auto it = std::find_if(widgets.begin(), widgets.end(), [&key](ElementInfoPartWidget *widget) {
return widget->key() == key;
});
QVERIFY2(it != widgets.end(), qPrintable(key));
(*it)->setText(context.value(key).toString().replace('\n', ' '));
}
const int undo_count = diagram->undoStack().count();
editor.apply();
QCOMPARE(element->elementInformations().value("model").toString(), QString("RX-24"));
QCOMPARE(diagram->undoStack().count(), undo_count + 1);
diagram->undoStack().undo();
QVERIFY(element->elementInformations().value("model").toString().isEmpty());
diagram->undoStack().redo();
QCOMPARE(element->elementInformations().value("voltage_rating").toString(), QString("24 V DC"));
QCOMPARE(DeviceBomExport::collect(project).first().m_device.m_model, QString("RX-24"));
editor.resize(600, 750);
editor.show();
QCoreApplication::processEvents();
const QString artifacts = qEnvironmentVariable("QET_SMART_DEVICE_ARTIFACT_DIR");
if (!artifacts.isEmpty()) {
QVERIFY(editor.grab().save(QDir(artifacts).filePath("properties.png")));
}
}
void existingDatabaseReports()
{
QETProject project;
auto *element = addElement(project, project.addNewDiagram(), "-K1");
QVERIFY(element);
element->setElementInformations(sample().toContext());
project.dataBase()->updateDB();
auto query = project.dataBase()->newQuery(QStringLiteral(
"SELECT model,category,voltage_rating,current_rating,notes FROM element_nomenclature_view"));
QVERIFY2(query.exec(), qPrintable(query.lastError().text()));
QVERIFY(query.next());
QCOMPARE(query.value(0).toString(), sample().m_model);
QCOMPARE(query.value(1).toString(), sample().m_category);
QCOMPARE(query.value(2).toString(), sample().m_voltage_rating);
QCOMPARE(query.value(3).toString(), sample().m_current_rating);
QCOMPARE(query.value(4).toString(), sample().m_notes);
}
private slots:
void metadataColumns();
void defaultQueryAndCsv();
void atomicWrite();
};
QTEST_MAIN(SmartDeviceTest)
void SmartDeviceTest::metadataColumns()
{
const auto columns = BomExport::defaultColumns();
for (const QString &column : {
QStringLiteral("label"), QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference"), QStringLiteral("model"),
QStringLiteral("description"), QStringLiteral("category"),
QStringLiteral("quantity"), QStringLiteral("voltage_rating"),
QStringLiteral("current_rating"), QStringLiteral("folio"),
QStringLiteral("notes")})
{
QVERIFY2(columns.contains(column), qPrintable(column));
}
}
void SmartDeviceTest::defaultQueryAndCsv()
{
const QString connection = QStringLiteral("smart-device-%1")
.arg(QUuid::createUuid().toString());
{
auto db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connection);
db.setDatabaseName(QStringLiteral(":memory:"));
QVERIFY2(db.open(), qPrintable(db.lastError().text()));
QSqlQuery setup(db);
QVERIFY(setup.exec(QStringLiteral(
"CREATE TABLE element_nomenclature_view ("
"label TEXT, designation TEXT, manufacturer TEXT, "
"manufacturer_reference TEXT, model TEXT, description TEXT, "
"category TEXT, quantity TEXT, voltage_rating TEXT, "
"current_rating TEXT, folio TEXT, notes TEXT, "
"element_type TEXT, diagram_position INTEGER, position INTEGER)")));
QSqlQuery insert(db);
insert.prepare(QStringLiteral(
"INSERT INTO element_nomenclature_view VALUES "
"(?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
auto add = [&insert](const QString &tag, const QString &type, int page,
int position, const QString &manufacturer) {
insert.bindValue(0, tag);
insert.bindValue(1, manufacturer);
insert.bindValue(2, QStringLiteral("0012"));
insert.bindValue(3, QStringLiteral("M-1"));
insert.bindValue(4, QStringLiteral("Power contactor"));
insert.bindValue(5, QStringLiteral("Control"));
insert.bindValue(6, QStringLiteral("1"));
insert.bindValue(7, QStringLiteral("24 V DC"));
insert.bindValue(8, QStringLiteral("16 A"));
insert.bindValue(9, QString::number(page));
insert.bindValue(10, QStringLiteral("Quoted \"note\"\nnext line"));
insert.bindValue(11, type);
insert.bindValue(12, page);
insert.bindValue(13, position);
return insert.exec();
};
QVERIFY(add(QStringLiteral("-K2"), QStringLiteral("simple"), 2, 1,
QStringLiteral("日本電機")));
QVERIFY(add(QStringLiteral("-K1"), QStringLiteral("master"), 1, 2,
QStringLiteral("Müller")));
QVERIFY(add(QStringLiteral("-K1.1"), QStringLiteral("slave"), 1, 3,
QStringLiteral("Must not be exported")));
QVERIFY(add(QStringLiteral("X1"), QStringLiteral("terminal"), 1, 4,
QStringLiteral("Must not be exported")));
QSqlQuery query(db);
QVERIFY2(query.exec(BomExport::defaultQuery()),
qPrintable(query.lastError().text()));
int rows = 0;
const auto csv = BomExport::toCsv(
query, BomExport::defaultColumns(), true, &rows);
QCOMPARE(rows, 2);
QVERIFY(csv.startsWith("\xEF\xBB\xBF\"label\";\"designation\";"));
QVERIFY(csv.contains(QStringLiteral("Müller").toUtf8()));
QVERIFY(csv.contains(QStringLiteral("日本電機").toUtf8()));
QVERIFY(csv.contains("\"Quoted \"\"note\"\"\nnext line\""));
QVERIFY(!csv.contains("Must not be exported"));
QVERIFY(csv.indexOf("-K1") < csv.indexOf("-K2"));
}
QSqlDatabase::removeDatabase(connection);
}
void SmartDeviceTest::atomicWrite()
{
QTemporaryDir temporary;
QVERIFY(temporary.isValid());
const auto path = temporary.filePath(QStringLiteral("bom.csv"));
const QByteArray csv("\xEF\xBB\xBF\"label\"\n");
QString error = QStringLiteral("stale");
QVERIFY(BomExport::writeCsv(path, csv, &error));
QVERIFY(error.isEmpty());
QFile file(path);
QVERIFY(file.open(QIODevice::ReadOnly));
QCOMPARE(file.readAll(), csv);
QVERIFY(!BomExport::writeCsv(
temporary.filePath(QStringLiteral("missing/bom.csv")), csv, &error));
QVERIFY(!error.isEmpty());
}
QTEST_GUILESS_MAIN(SmartDeviceTest)
#include "tst_smart_device.moc"