mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-18 03:54:14 +02:00
Merge pull request #1491 from ispyisail/fix/reattach-wires-moved-terminals
Fix wires lost when a symbol's terminal moved a few units
This commit is contained in:
@@ -1790,6 +1790,11 @@ QList<Terminal *> findTerminals(int conductor_index,
|
||||
if (terminal->derivedUuid() == terminal_uuid)
|
||||
match = terminal;
|
||||
}
|
||||
//A terminal that moved since the project was saved: the
|
||||
//uuid worked out from its old place was paired with it
|
||||
//when the symbol was read (Element::fromXml()).
|
||||
if (!match)
|
||||
match = element->terminalFromOldPlace(terminal_uuid);
|
||||
if (match)
|
||||
found << match;
|
||||
else
|
||||
@@ -2241,6 +2246,7 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
if (consider_informations)
|
||||
m_wires_not_reconnected.clear();
|
||||
QList<Conductor *> added_conductors;
|
||||
int moved_ends = 0;
|
||||
for (auto f : QET::findInDomElement(root,
|
||||
QStringLiteral("conductors"),
|
||||
QStringLiteral("conductor")))
|
||||
@@ -2312,11 +2318,19 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
ends.join(QLatin1Char('\n'))));
|
||||
}
|
||||
added_conductors << c;
|
||||
for (Terminal *end : {p1, p2})
|
||||
if (end->parentElement()->terminalWasMoved(end)) ++moved_ends;
|
||||
}
|
||||
else
|
||||
delete c;
|
||||
}
|
||||
}
|
||||
if (consider_informations && moved_ends) {
|
||||
qInfo().noquote() << "Diagram::fromXml():"
|
||||
<< moved_ends
|
||||
<< "wire end(s) on a terminal that moved since the project was saved,"
|
||||
<< "matched to it by place";
|
||||
}
|
||||
if (consider_informations && !m_wires_not_reconnected.isEmpty()) {
|
||||
qWarning().noquote() << "Diagram::fromXml():"
|
||||
<< m_wires_not_reconnected.size()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "../qetapp.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../dataBase/projectdatabase.h"
|
||||
#include "../ElementsCollection/terminaluuids.h"
|
||||
#include "../PropertiesEditor/propertieseditordialog.h"
|
||||
#include "../autoNum/assignvariables.h"
|
||||
#include "../autoNum/elementautonumschemecommand.h"
|
||||
@@ -972,6 +973,182 @@ bool Element::valideXml(QDomElement &e)
|
||||
return(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::matchMovedTerminals
|
||||
Pair the saved terminals that no live terminal matched exactly with the
|
||||
live terminals that moved a little: the definition of the symbol was
|
||||
replaced or redrawn since the project was saved. A saved record gives
|
||||
the old docking point and orientation of its terminal, so it is paired
|
||||
with a live terminal of the same orientation whose docking point is
|
||||
within moved_terminal_reach in x and y. Two steps, both only where the
|
||||
pairing is clear:
|
||||
- a rigid offset: if exactly one offset (the same for every terminal)
|
||||
maps every unmatched record onto a distinct live terminal, as when a
|
||||
whole drawing was moved, all are paired by it;
|
||||
- otherwise per terminal: a record and a live terminal that are each
|
||||
the only candidate of the other. Several records at one place and as
|
||||
many live terminals at one place are paired in order.
|
||||
Anything less clear stays unmatched, as before.
|
||||
|
||||
A pair registers the record's legacy id, and the uuid TerminalUuids
|
||||
derived for the terminal at its old place, so that a wire naming either
|
||||
reattaches (see Diagram::findTerminals()).
|
||||
@param records : the saved terminals of this symbol
|
||||
@param matched_records : indexes in records already matched
|
||||
@param matched_terminals : the live terminals already matched
|
||||
@param id_adr : receives the legacy id of each pair
|
||||
*/
|
||||
void Element::matchMovedTerminals(const QList<QDomElement> &records,
|
||||
const QSet<int> &matched_records,
|
||||
const QSet<Terminal *> &matched_terminals,
|
||||
QHash<int, Terminal *> &id_adr)
|
||||
{
|
||||
if (matched_records.size() == records.size()) return;
|
||||
|
||||
const auto dock_of = [](const QDomElement &r) {
|
||||
return QPointF(r.attribute(QStringLiteral("x")).toDouble(),
|
||||
r.attribute(QStringLiteral("y")).toDouble());
|
||||
};
|
||||
const auto orientation_of = [](const QDomElement &r) {
|
||||
return r.attribute(QStringLiteral("orientation")).toInt();
|
||||
};
|
||||
|
||||
QHash<int, QList<Terminal *>> candidates; // record -> live terminals in reach
|
||||
QHash<Terminal *, QList<int>> claimants; // live terminal -> records in reach
|
||||
for (int i = 0; i < records.size(); ++i) {
|
||||
if (matched_records.contains(i)) continue;
|
||||
const QPointF dock = dock_of(records.at(i));
|
||||
for (Terminal *t : std::as_const(m_terminals)) {
|
||||
if (matched_terminals.contains(t)) continue;
|
||||
if (static_cast<int>(t->orientation()) != orientation_of(records.at(i))) continue;
|
||||
if (qAbs(t->dock_elmt_.x() - dock.x()) > moved_terminal_reach
|
||||
|| qAbs(t->dock_elmt_.y() - dock.y()) > moved_terminal_reach) continue;
|
||||
candidates[i] << t;
|
||||
claimants[t] << i;
|
||||
}
|
||||
}
|
||||
|
||||
//Pair a record with a live terminal: register its legacy id, and the
|
||||
//uuid derived for the terminal at its old place
|
||||
const auto pair_up = [&](int index, Terminal *t) {
|
||||
const QDomElement &record = records.at(index);
|
||||
const QPointF dock = dock_of(record);
|
||||
|
||||
//The old place of the terminal: its docking point less the
|
||||
//stub that Terminal::init() added
|
||||
QPointF old_pos = dock;
|
||||
switch (orientation_of(record)) {
|
||||
case Qet::North: old_pos -= QPointF(0, Terminal::terminalSize); break;
|
||||
case Qet::East : old_pos -= QPointF(-Terminal::terminalSize, 0); break;
|
||||
case Qet::West : old_pos -= QPointF(Terminal::terminalSize, 0); break;
|
||||
case Qet::South: old_pos -= QPointF(0, -Terminal::terminalSize); break;
|
||||
}
|
||||
//The rank among the records at the same place, as the project
|
||||
//counted it when it gave the terminals their uuids
|
||||
int rank = 0;
|
||||
for (int j = 0; j < index; ++j) {
|
||||
const QPointF other = dock_of(records.at(j));
|
||||
if (orientation_of(records.at(j)) == orientation_of(record)
|
||||
&& qAbs(other.x() - dock.x()) < 1e-6
|
||||
&& qAbs(other.y() - dock.y()) < 1e-6)
|
||||
++rank;
|
||||
}
|
||||
|
||||
id_adr.insert(record.attribute(QStringLiteral("id")).toInt(), t);
|
||||
m_old_place_terminals.insert(TerminalUuids::derived(old_pos.x(), old_pos.y(),
|
||||
orientation_of(record), rank), t);
|
||||
m_moved_terminals << t;
|
||||
};
|
||||
const auto same_place = [](const QPointF &a, const QPointF &b) {
|
||||
return qAbs(a.x() - b.x()) < 1e-6 && qAbs(a.y() - b.y()) < 1e-6;
|
||||
};
|
||||
|
||||
//Rigid offset: when a whole drawing was moved, every terminal moved
|
||||
//by the same amount. Try each offset seen between a record and a
|
||||
//live terminal; if exactly one maps every unmatched record onto a
|
||||
//distinct live terminal, the pairs are settled by it.
|
||||
QList<int> unmatched;
|
||||
QList<QPointF> offsets;
|
||||
for (int i = 0; i < records.size(); ++i) {
|
||||
if (matched_records.contains(i)) continue;
|
||||
unmatched << i;
|
||||
for (Terminal *t : candidates.value(i)) {
|
||||
const QPointF offset = t->dock_elmt_ - dock_of(records.at(i));
|
||||
const bool known = std::any_of(offsets.cbegin(), offsets.cend(),
|
||||
[&](const QPointF &o) { return same_place(o, offset); });
|
||||
if (!known) offsets << offset;
|
||||
}
|
||||
}
|
||||
QList<QPair<int, Terminal *>> rigid;
|
||||
int rigid_offsets = 0;
|
||||
for (const QPointF &offset : std::as_const(offsets)) {
|
||||
QList<QPair<int, Terminal *>> mapping;
|
||||
QSet<Terminal *> used;
|
||||
for (int i : std::as_const(unmatched)) {
|
||||
Terminal *found = nullptr; // records at one place take the live ones in order
|
||||
for (Terminal *t : candidates.value(i)) {
|
||||
if (!used.contains(t)
|
||||
&& same_place(t->dock_elmt_, dock_of(records.at(i)) + offset)) {
|
||||
found = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) break;
|
||||
used << found;
|
||||
mapping.append({i, found});
|
||||
}
|
||||
if (mapping.size() != unmatched.size()) continue;
|
||||
++rigid_offsets;
|
||||
rigid = mapping;
|
||||
}
|
||||
if (rigid_offsets == 1) {
|
||||
for (const auto &pair : std::as_const(rigid)) pair_up(pair.first, pair.second);
|
||||
return;
|
||||
}
|
||||
|
||||
//Per terminal: a record and a live terminal that are each the only
|
||||
//candidate of the other. Several records at one place with as many
|
||||
//live terminals at one place pair in order.
|
||||
for (int i : std::as_const(unmatched)) {
|
||||
const QList<Terminal *> live = candidates.value(i);
|
||||
if (live.isEmpty()) continue;
|
||||
QList<int> group;
|
||||
for (int j : std::as_const(unmatched)) {
|
||||
if (orientation_of(records.at(j)) == orientation_of(records.at(i))
|
||||
&& same_place(dock_of(records.at(j)), dock_of(records.at(i))))
|
||||
group << j;
|
||||
}
|
||||
if (group.first() != i || live.size() != group.size()) continue;
|
||||
bool clear = true;
|
||||
for (Terminal *t : live) {
|
||||
if (!same_place(t->dock_elmt_, live.first()->dock_elmt_)
|
||||
|| claimants.value(t) != group) clear = false;
|
||||
}
|
||||
if (!clear) continue;
|
||||
for (int k = 0; k < group.size(); ++k) pair_up(group.at(k), live.at(k));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@return the terminal that was paired with the saved terminal whose uuid
|
||||
derived from its old place is @p old_place_uuid, if any
|
||||
@see matchMovedTerminals()
|
||||
*/
|
||||
Terminal *Element::terminalFromOldPlace(const QUuid &old_place_uuid) const
|
||||
{
|
||||
return m_old_place_terminals.value(old_place_uuid, nullptr);
|
||||
}
|
||||
|
||||
/**
|
||||
@return true if @p terminal was paired with a saved terminal at another
|
||||
place than its own
|
||||
@see matchMovedTerminals()
|
||||
*/
|
||||
bool Element::terminalWasMoved(const Terminal *terminal) const
|
||||
{
|
||||
return m_moved_terminals.contains(const_cast<Terminal *>(terminal));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Element::fromXml
|
||||
Import the parameters of this element from a xml document.
|
||||
@@ -999,22 +1176,28 @@ bool Element::fromXml(QDomElement &e,
|
||||
}
|
||||
|
||||
QHash<int, Terminal *> priv_id_adr;
|
||||
QSet<Terminal *> matched_terminals;
|
||||
QSet<int> matched_records;
|
||||
|
||||
for (auto *qgi : childItems())
|
||||
{
|
||||
if (auto terminal_ = qgraphicsitem_cast<Terminal *>(qgi))
|
||||
{
|
||||
for(auto qde : liste_terminals)
|
||||
for (int i = 0; i < liste_terminals.size(); ++i)
|
||||
{
|
||||
QDomElement qde = liste_terminals.at(i);
|
||||
if (terminal_ -> fromXml(qde))
|
||||
{
|
||||
priv_id_adr.insert(qde.attribute(QStringLiteral("id")).toInt(),
|
||||
terminal_);
|
||||
matched_terminals << terminal_;
|
||||
matched_records << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchMovedTerminals(liste_terminals, matched_records, matched_terminals, priv_id_adr);
|
||||
|
||||
//Check that associated id/address doesn't conflict with table_id_adr
|
||||
for(auto found_id : priv_id_adr.keys())
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QPicture>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -282,6 +283,23 @@ class Element : public QetGraphicsItem
|
||||
DynamicElementTextItem *parseDynamicText(
|
||||
const QDomElement &dom_element);
|
||||
Terminal *parseTerminal(const QDomElement &dom_element);
|
||||
void matchMovedTerminals(const QList<QDomElement> &records,
|
||||
const QSet<int> &matched_records,
|
||||
const QSet<Terminal *> &matched_terminals,
|
||||
QHash<int, Terminal *> &id_adr);
|
||||
|
||||
public:
|
||||
Terminal *terminalFromOldPlace(const QUuid &old_place_uuid) const;
|
||||
bool terminalWasMoved(const Terminal *terminal) const;
|
||||
|
||||
private:
|
||||
/// How far, in x and y, a terminal may have moved from where
|
||||
/// a project saved it and still be recognised
|
||||
static constexpr qreal moved_terminal_reach = 5.0;
|
||||
/// Terminals paired with a saved one that was elsewhere,
|
||||
/// and the uuid derived from that old place
|
||||
QHash<QUuid, Terminal *> m_old_place_terminals;
|
||||
QSet<Terminal *> m_moved_terminals;
|
||||
|
||||
//Reimplemented from QGraphicsItem
|
||||
public:
|
||||
|
||||
@@ -1033,6 +1033,18 @@ target_compile_definitions(tst_derivedwireuuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A wire follows a terminal that moved a little since the project was saved
|
||||
# (a symbol replaced by a newer definition), by its legacy id or by the uuid
|
||||
# derived from its old place: Element::fromXml() pairs them (F112).
|
||||
add_executable(
|
||||
tst_movedterminals
|
||||
tst_movedterminals.cpp)
|
||||
add_test(NAME tst_movedterminals COMMAND tst_movedterminals)
|
||||
add_dependencies(tst_movedterminals qelectrotech)
|
||||
target_link_libraries(tst_movedterminals PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_movedterminals PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# Symbols sharing one uuid on a folio keep their wires through save and
|
||||
# reopen (#1408): nets of examples/Habitat-Schemas_developpes.qet.
|
||||
add_executable(
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
|
||||
// A project saves, for every placed symbol, where each terminal docked, and
|
||||
// its wires name the terminals by that number or by the uuid worked out from
|
||||
// that place. When the symbol's definition has since been replaced by one
|
||||
// whose terminal sits a little elsewhere, the wire must still reattach --
|
||||
// when the match is clear.
|
||||
//
|
||||
// Runs the real binary (--resave) on a small project written here: two
|
||||
// copies of one embedded symbol, joined by one wire from the south terminal
|
||||
// (the one the definition moves) to the north one (which stays).
|
||||
namespace {
|
||||
|
||||
const QUuid ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
|
||||
// What TerminalUuids::derived() works out for a terminal at x, y
|
||||
QString derived(int y, int orientation)
|
||||
{
|
||||
return QUuid::createUuidV5(ns, QStringLiteral("%1|%2|%3")
|
||||
.arg(0.0, 0, 'f', 4).arg(double(y), 0, 'f', 4).arg(orientation))
|
||||
.toString();
|
||||
}
|
||||
|
||||
const QString uuid_a = QStringLiteral("{11111111-1111-4111-8111-111111111111}");
|
||||
const QString uuid_b = QStringLiteral("{22222222-2222-4222-8222-222222222222}");
|
||||
|
||||
// The symbol as the project's embedded collection now holds it. The south
|
||||
// terminals are at the given y (the project was saved with one at y=10);
|
||||
// the north one has always been at y=-10.
|
||||
QString definition(const QList<int> &south_ys)
|
||||
{
|
||||
QString terminals = QStringLiteral(
|
||||
"<terminal orientation=\"n\" type=\"Generic\" name=\"n\" x=\"0\" y=\"-10\"/>");
|
||||
for (int y : south_ys)
|
||||
terminals += QStringLiteral(
|
||||
"<terminal orientation=\"s\" type=\"Generic\" name=\"s\" x=\"0\" y=\"%1\"/>").arg(y);
|
||||
return QStringLiteral(
|
||||
"<collection><category name=\"import\"><category name=\"t\">"
|
||||
"<element name=\"box.elmt\">"
|
||||
"<definition width=\"30\" hotspot_y=\"21\" version=\"0.100.0\" type=\"element\""
|
||||
" link_type=\"simple\" hotspot_x=\"15\" height=\"42\">"
|
||||
"<uuid uuid=\"{33333333-3333-4333-8333-333333333333}\"/>"
|
||||
"<names><name lang=\"en\">box</name></names>"
|
||||
"<kindInformations/><informations/>"
|
||||
"<description><rect x=\"-5\" y=\"-6\" width=\"10\" height=\"12\" antialias=\"true\""
|
||||
" style=\"line-style:normal;line-weight:normal;filling:none;color:black\"/>%1</description>"
|
||||
"</definition></element></category></category></collection>").arg(terminals);
|
||||
}
|
||||
|
||||
// One placed symbol as the project saved it: south terminal docked at y=6
|
||||
// (a terminal at y=10 less the 4-unit stub), north at y=-6.
|
||||
QString placed(const QString &uuid, int first_id, int x)
|
||||
{
|
||||
return QStringLiteral(
|
||||
"<element orientation=\"0\" type=\"embed://import/t/box.elmt\" z=\"10\""
|
||||
" uuid=\"%1\" x=\"%2\" y=\"200\"><terminals>"
|
||||
"<terminal orientation=\"2\" x=\"0\" y=\"6\" id=\"%3\"/>"
|
||||
"<terminal orientation=\"0\" x=\"0\" y=\"-6\" id=\"%4\"/>"
|
||||
"</terminals></element>").arg(uuid).arg(x).arg(first_id).arg(first_id + 1);
|
||||
}
|
||||
|
||||
// The project, with the wire naming its ends as given
|
||||
QString project(const QList<int> &south_ys, const QString &wire_ends)
|
||||
{
|
||||
return QStringLiteral(
|
||||
"<project title=\"moved\" version=\"0.100.1\">"
|
||||
"<diagram title=\"f\" cols=\"8\" rows=\"4\" colsize=\"60\" rowsize=\"80\">"
|
||||
"<elements>%1%2</elements>"
|
||||
"<conductors><conductor %3 type=\"multi\" num=\"\"/></conductors>"
|
||||
"</diagram>%4</project>")
|
||||
.arg(placed(uuid_a, 0, 100), placed(uuid_b, 2, 300), wire_ends, definition(south_ys));
|
||||
}
|
||||
|
||||
QString byId() { return QStringLiteral("terminal1=\"0\" terminal2=\"3\""); }
|
||||
QString byUuid(int old_south_y)
|
||||
{
|
||||
return QStringLiteral("element1=\"%1\" terminal1=\"%2\" element2=\"%3\" terminal2=\"%4\"")
|
||||
.arg(uuid_a, derived(old_south_y, 2), uuid_b, derived(-10, 0));
|
||||
}
|
||||
|
||||
|
||||
// A symbol with any terminals, for the cases of several terminals moved at
|
||||
// once. A terminal is its orientation letter and its position.
|
||||
struct Term { QChar o; int x; int y; };
|
||||
|
||||
QString multiDefinition(const QList<Term> &terms)
|
||||
{
|
||||
QString out;
|
||||
for (const Term &t : terms)
|
||||
out += QStringLiteral("<terminal orientation=\"%1\" type=\"Generic\" name=\"t\""
|
||||
" x=\"%2\" y=\"%3\"/>").arg(t.o).arg(t.x).arg(t.y);
|
||||
QString d = definition({});
|
||||
// the lone north terminal of definition() is replaced
|
||||
d.replace(QRegularExpression(QStringLiteral("<terminal [^>]*/>")), QString());
|
||||
return d.replace(QStringLiteral("</description>"), out + QStringLiteral("</description>"));
|
||||
}
|
||||
|
||||
// A placed symbol whose terminals were saved at the given positions (ids
|
||||
// first_id, first_id + 1, ...). The saved docking point is the position less
|
||||
// the stub of 4: north y+4, south y-4, east x-4, west x+4.
|
||||
QString multiPlaced(const QString &uuid, int first_id, int x, const QList<Term> &saved)
|
||||
{
|
||||
QString out;
|
||||
int id = first_id;
|
||||
for (const Term &t : saved) {
|
||||
int dx = t.x, dy = t.y, o = 0;
|
||||
switch (t.o.toLatin1()) {
|
||||
case 'n': dy += 4; o = 0; break;
|
||||
case 'e': dx -= 4; o = 1; break;
|
||||
case 's': dy -= 4; o = 2; break;
|
||||
default : dx += 4; o = 3; break;
|
||||
}
|
||||
out += QStringLiteral("<terminal orientation=\"%1\" x=\"%2\" y=\"%3\" id=\"%4\"/>")
|
||||
.arg(o).arg(dx).arg(dy).arg(id++);
|
||||
}
|
||||
return QStringLiteral(
|
||||
"<element orientation=\"0\" type=\"embed://import/t/box.elmt\" z=\"10\""
|
||||
" uuid=\"%1\" x=\"%2\" y=\"200\"><terminals>%3</terminals></element>")
|
||||
.arg(uuid).arg(x).arg(out);
|
||||
}
|
||||
|
||||
// Two copies of the symbol saved with terminals `saved`, now defined with
|
||||
// `now`; one wire joins each terminal of the first copy to the same terminal
|
||||
// of the second (by legacy id).
|
||||
QString multiProject(const QList<Term> &saved, const QList<Term> &now)
|
||||
{
|
||||
QString wires;
|
||||
for (int i = 0; i < saved.size(); ++i)
|
||||
wires += QStringLiteral("<conductor terminal1=\"%1\" terminal2=\"%2\" type=\"multi\" num=\"\"/>")
|
||||
.arg(i).arg(saved.size() + i);
|
||||
return QStringLiteral(
|
||||
"<project title=\"moved\" version=\"0.100.1\">"
|
||||
"<diagram title=\"f\" cols=\"8\" rows=\"4\" colsize=\"60\" rowsize=\"80\">"
|
||||
"<elements>%1%2</elements><conductors>%3</conductors></diagram>%4</project>")
|
||||
.arg(multiPlaced(uuid_a, 0, 100, saved),
|
||||
multiPlaced(uuid_b, saved.size(), 300, saved),
|
||||
wires, multiDefinition(now));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class tst_movedterminals : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
struct Result { int wires = -1; QString log; QStringList pairs; };
|
||||
|
||||
// Open and save the project in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead): the wires that survive.
|
||||
Result resave(const QString &xml)
|
||||
{
|
||||
const QString in = m_dir.filePath(QStringLiteral("in%1.qet").arg(m_run));
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QFile f(in);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(xml.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0) return {};
|
||||
Result r;
|
||||
r.log = QString::fromUtf8(proc.readAllStandardError() + proc.readAllStandardOutput());
|
||||
QDomDocument doc;
|
||||
QFile saved(out);
|
||||
if (!saved.open(QIODevice::ReadOnly) || !doc.setContent(&saved)) return {};
|
||||
r.wires = doc.elementsByTagName(QStringLiteral("conductor")).size();
|
||||
for (int i = 0; i < r.wires; ++i) {
|
||||
const QDomElement c = doc.elementsByTagName(QStringLiteral("conductor")).at(i).toElement();
|
||||
r.pairs << c.attribute(QStringLiteral("terminal1")) + QLatin1Char('>') + c.attribute(QStringLiteral("terminal2"));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static bool said(const Result &r)
|
||||
{
|
||||
return r.log.contains(QStringLiteral("matched to it by place"));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
// The terminal has not moved: found as before, nothing to report.
|
||||
void exactMatchIsUnaffected()
|
||||
{
|
||||
const Result legacy = resave(project({10}, byId()));
|
||||
QCOMPARE(legacy.wires, 1);
|
||||
QVERIFY(!said(legacy));
|
||||
const Result uuid = resave(project({10}, byUuid(10)));
|
||||
QCOMPARE(uuid.wires, 1);
|
||||
QVERIFY(!said(uuid));
|
||||
}
|
||||
|
||||
void legacyIdWireFollowsATerminalMovedThreeUnits()
|
||||
{
|
||||
const Result r = resave(project({13}, byId()));
|
||||
QCOMPARE(r.wires, 1);
|
||||
QVERIFY(said(r));
|
||||
}
|
||||
|
||||
void derivedUuidWireFollowsATerminalMovedThreeUnits()
|
||||
{
|
||||
const Result r = resave(project({13}, byUuid(10)));
|
||||
QCOMPARE(r.wires, 1);
|
||||
QVERIFY(said(r));
|
||||
}
|
||||
|
||||
void movedFiveUnitsStillMatches()
|
||||
{
|
||||
QCOMPARE(resave(project({15}, byId())).wires, 1);
|
||||
}
|
||||
|
||||
void movedSixUnitsDoesNot()
|
||||
{
|
||||
QCOMPARE(resave(project({16}, byId())).wires, 0);
|
||||
QCOMPARE(resave(project({16}, byUuid(10))).wires, 0);
|
||||
}
|
||||
|
||||
// Two live terminals in reach of one saved one: no way to tell which.
|
||||
void twoCandidatesPairNothing()
|
||||
{
|
||||
const Result legacy = resave(project({13, 14}, byId()));
|
||||
QCOMPARE(legacy.wires, 0);
|
||||
QVERIFY(!said(legacy));
|
||||
QCOMPARE(resave(project({13, 14}, byUuid(10))).wires, 0);
|
||||
}
|
||||
|
||||
// A whole drawing moved by 5: two terminals 10 apart, so each saved
|
||||
// one has two live terminals at 5 units. Only one shift fits both.
|
||||
void wholeDrawingMovedPairsBothTerminals()
|
||||
{
|
||||
const Result r = resave(multiProject({{'n', -5, -10}, {'n', 5, -10}},
|
||||
{{'n', -10, -10}, {'n', 0, -10}}));
|
||||
QCOMPARE(r.wires, 2);
|
||||
QVERIFY(said(r));
|
||||
}
|
||||
|
||||
// Two terminals at one place, moved by 1: paired in order.
|
||||
void terminalsAtOnePlaceKeepTheirOrder()
|
||||
{
|
||||
const QList<Term> saved = {{'n', 1, -40}, {'n', 1, -40}};
|
||||
const Result r = resave(multiProject(saved, {{'n', 0, -40}, {'n', 0, -40}}));
|
||||
QCOMPARE(r.wires, 2);
|
||||
QVERIFY(said(r));
|
||||
// each wire runs between the same terminal of both copies, and the
|
||||
// first saved one took the first live one (the rank-0 uuid)
|
||||
QCOMPARE(r.pairs.size(), 2);
|
||||
QCOMPARE(r.pairs.at(0), derived(-40, 0) + QLatin1Char('>') + derived(-40, 0));
|
||||
QVERIFY(r.pairs.at(1) != r.pairs.at(0));
|
||||
const QStringList second = r.pairs.at(1).split(QLatin1Char('>'));
|
||||
QCOMPARE(second.at(0), second.at(1));
|
||||
}
|
||||
|
||||
// Terminals that moved by different amounts have no common shift:
|
||||
// each is still paired by itself.
|
||||
void differentShiftsStillPairPerTerminal()
|
||||
{
|
||||
const Result r = resave(multiProject({{'n', -20, -10}, {'n', 20, -10}},
|
||||
{{'n', -18, -10}, {'n', 23, -10}}));
|
||||
QCOMPARE(r.wires, 2);
|
||||
}
|
||||
|
||||
// A terminal that moved while another did not leaves a lone saved
|
||||
// terminal: no rigid shift is needed, the per-terminal rule pairs it.
|
||||
void oneMovedAmongStaysStillPairs()
|
||||
{
|
||||
QCOMPARE(resave(multiProject({{'n', -20, -10}, {'n', 20, -10}},
|
||||
{{'n', -20, -10}, {'n', 23, -10}})).wires, 2);
|
||||
}
|
||||
|
||||
// Still ambiguous: the two saved terminals could each be either live
|
||||
// one, and no single shift maps both.
|
||||
void ambiguousStillRefuses()
|
||||
{
|
||||
QCOMPARE(resave(multiProject({{'n', 0, -10}, {'n', 4, -10}},
|
||||
{{'n', 2, -10}, {'n', 6, -10}, {'n', -2, -10}})).wires, 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_movedterminals)
|
||||
|
||||
#include "tst_movedterminals.moc"
|
||||
Reference in New Issue
Block a user