mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
Merge pull request #1118 from ispyisail/feat/fill-terminal-uuids
Give terminals in older projects a lasting uuid
This commit is contained in:
+75
-5
@@ -126,12 +126,56 @@ def _wires(diagram: ET.Element):
|
||||
|
||||
|
||||
def _conductors(root: ET.Element):
|
||||
definitions = _definition_terminals(root)
|
||||
for i, d in _folios(root):
|
||||
index = _terminal_index(d)
|
||||
index = _terminal_index(d, definitions)
|
||||
for c in _wires(d):
|
||||
yield i, c, index
|
||||
|
||||
|
||||
def _uuid_key(value: str) -> str:
|
||||
return (value or "").strip().strip("{}").lower()
|
||||
|
||||
|
||||
# Orientation as a placed symbol's <terminal> record writes it (an int,
|
||||
# Qet::Orientation) or as a definition does (n/e/s/w).
|
||||
_ORIENTATIONS = {"n": 0, "e": 1, "s": 2, "w": 3, "0": 0, "1": 1, "2": 2, "3": 3}
|
||||
|
||||
# Where QElectroTech docks a wire, relative to the terminal's position in
|
||||
# its definition (Terminal's constructor, Terminal::terminalSize = 4). A
|
||||
# placed symbol's <terminal> record is written at that point.
|
||||
_DOCK_OFFSET = {0: (0.0, 4.0), 1: (-4.0, 0.0), 2: (0.0, -4.0), 3: (4.0, 0.0)}
|
||||
|
||||
|
||||
def _definition_terminals(root: ET.Element) -> dict:
|
||||
"""Map each symbol stored in the project ("embed://" + its path in the
|
||||
<collection>) to its terminals: {terminal uuid: (x, y, orientation)},
|
||||
the position being the one in the definition."""
|
||||
out = {}
|
||||
|
||||
def walk(node, path):
|
||||
for child in node:
|
||||
if child.tag == "category":
|
||||
walk(child, path + [child.get("name", "")])
|
||||
elif child.tag == "element":
|
||||
terminals = {}
|
||||
for t in child.findall("definition/description/terminal"):
|
||||
try:
|
||||
terminals[_uuid_key(t.get("uuid"))] = (
|
||||
float(t.get("x")), float(t.get("y")),
|
||||
_ORIENTATIONS.get((t.get("orientation") or "n")[:1], 0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
terminals.pop("", None)
|
||||
if terminals:
|
||||
out["embed://" + "/".join(path + [child.get("name", "")])] = terminals
|
||||
|
||||
collection = root.find("collection")
|
||||
if collection is not None:
|
||||
walk(collection, [])
|
||||
return out
|
||||
|
||||
|
||||
def _element_row(folio: int, el: ET.Element) -> dict:
|
||||
info = _element_info(el)
|
||||
etype = el.get("type", "")
|
||||
@@ -147,7 +191,7 @@ def _element_row(folio: int, el: ET.Element) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _terminal_index(diagram: ET.Element) -> dict:
|
||||
def _terminal_index(diagram: ET.Element, definitions: dict | None = None) -> dict:
|
||||
"""Map a folio's terminal ids to an identity that survives a save.
|
||||
|
||||
A conductor names its ends with terminal1/terminal2, which are plain
|
||||
@@ -170,6 +214,17 @@ def _terminal_index(diagram: ET.Element) -> dict:
|
||||
Conductors in the corpus carry no element1/element2 attribute -- 0 of
|
||||
47 in ArduinoLCD.qet, 0 of 67 in 741.qet -- so this mapping has to be
|
||||
built from the elements rather than read off the conductor.
|
||||
|
||||
A conductor can also name its ends by terminal uuid (element1 +
|
||||
terminal1), and QElectroTech writes that form as soon as the terminal
|
||||
has a uuid -- which, since a project gives every terminal one on
|
||||
opening, is the first save of any older file. So the same untouched
|
||||
conductor is written in the numbered form before a save and the uuid
|
||||
form after it. With @p definitions (from _definition_terminals()), a
|
||||
uuid end is resolved too, keyed (element uuid, terminal uuid), to the
|
||||
very same identity as the numbered end: the terminal's definition
|
||||
position, moved to where the wire docks, is where the placed symbol's
|
||||
<terminal> record is.
|
||||
"""
|
||||
index = {}
|
||||
for el in diagram.iter("element"):
|
||||
@@ -184,12 +239,26 @@ def _terminal_index(diagram: ET.Element) -> dict:
|
||||
# marked with a "#" so the caller can see the diff is on the
|
||||
# unstable footing that file forces.
|
||||
continue
|
||||
records = []
|
||||
for t in el.iter("terminal"):
|
||||
tid = t.get("id")
|
||||
if tid is None:
|
||||
continue
|
||||
index[tid] = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
|
||||
f",{t.get('orientation','?')}")
|
||||
key = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
|
||||
f",{t.get('orientation','?')}")
|
||||
index[tid] = key
|
||||
records.append((t, key))
|
||||
for tuuid, (x, y, o) in (definitions or {}).get(el.get("type", ""), {}).items():
|
||||
dx, dy = _DOCK_OFFSET[o]
|
||||
for t, key in records:
|
||||
try:
|
||||
if (abs(float(t.get("x")) - (x + dx)) < 1e-6
|
||||
and abs(float(t.get("y")) - (y + dy)) < 1e-6
|
||||
and _ORIENTATIONS.get((t.get("orientation") or "")[:1]) == o):
|
||||
index[(_uuid_key(uuid), tuuid)] = key
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return index
|
||||
|
||||
|
||||
@@ -214,7 +283,8 @@ def _conductor_key(folio: int, c: ET.Element, index: dict) -> str:
|
||||
tid = c.get(term_attr, "?")
|
||||
owner = c.get(elem_attr)
|
||||
if owner:
|
||||
ends.append(f"{owner}/{tid or c.get(name_attr, '?')}")
|
||||
ends.append(index.get((_uuid_key(owner), _uuid_key(tid)))
|
||||
or f"{owner}/{tid or c.get(name_attr, '?')}")
|
||||
else:
|
||||
# An id with no element behind it stays visible as itself
|
||||
# rather than silently collapsing conductors onto one key.
|
||||
|
||||
@@ -901,6 +901,30 @@ class ReadToolContracts(unittest.TestCase):
|
||||
r = tool(str(big))
|
||||
self.assertEqual((r["count"], r["truncated"], len(r[key])), (201, True, 200))
|
||||
|
||||
def test_conductor_key_same_in_both_forms(self):
|
||||
# A wire in the numbered form before a save and the uuid form after
|
||||
# it (the first save of an older project) is the same wire: both
|
||||
# ends resolve to the placed symbol's terminal record. The record is
|
||||
# where the wire docks, 4 from the definition position.
|
||||
root = ET.fromstring(
|
||||
'<project><collection><category name="import"><category name="x">'
|
||||
'<element name="S.elmt"><definition><description>'
|
||||
'<terminal uuid="{T1}" x="0" y="0" orientation="s"/>'
|
||||
'<terminal uuid="{T2}" x="10" y="0" orientation="e"/>'
|
||||
'</description></definition></element>'
|
||||
'</category></category></collection>'
|
||||
'<diagram><elements><element uuid="{E}" type="embed://import/x/S.elmt">'
|
||||
'<terminals><terminal id="5" x="0" y="-4" orientation="2"/>'
|
||||
'<terminal id="6" x="6" y="0" orientation="1"/></terminals>'
|
||||
'</element></elements><conductors>'
|
||||
'<conductor terminal1="5" terminal2="6"/>'
|
||||
'<conductor element1="{e}" terminal1="{t1}" element2="{E}" terminal2="{T2}"/>'
|
||||
'</conductors></diagram></project>')
|
||||
numbered, by_uuid = [m._conductor_row(i, c, ix)["key"]
|
||||
for i, c, ix in m._conductors(root)]
|
||||
self.assertEqual(numbered, "1:{E}@0,-4,2--{E}@6,0,1")
|
||||
self.assertEqual(by_uuid, numbered)
|
||||
|
||||
def test_conductor_row_without_an_index(self):
|
||||
c = ET.fromstring('<conductor terminal1="7" terminal2="8"/>')
|
||||
self.assertEqual(m._conductor_row(3, c)["key"], "3:#7--#8")
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
#include <QUuid>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
@@ -49,6 +50,67 @@ QString terminalPlace(const QDomElement &terminal)
|
||||
QString::number(terminal.attribute(QStringLiteral("y")).toDouble()),
|
||||
terminal.attribute(QStringLiteral("orientation")));
|
||||
}
|
||||
|
||||
//Qet::orientationFromString(), without pulling in qet.cpp
|
||||
int orientationOf(const QDomElement &terminal)
|
||||
{
|
||||
const QString o = terminal.attribute(QStringLiteral("orientation"));
|
||||
if (o.startsWith(QLatin1Char('e'))) return 1;
|
||||
if (o.startsWith(QLatin1Char('s'))) return 2;
|
||||
if (o.startsWith(QLatin1Char('w'))) return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
//The <terminal> of one element definition get a uuid where missing
|
||||
int fillDefinition(const QDomElement &collection_element)
|
||||
{
|
||||
const QList<QDomElement> terminals = terminalsOf(collection_element);
|
||||
QSet<QUuid> taken;
|
||||
for (const QDomElement &t : terminals) {
|
||||
const QUuid uuid(t.attribute(QStringLiteral("uuid")));
|
||||
if (!uuid.isNull()) {
|
||||
taken << uuid;
|
||||
}
|
||||
}
|
||||
|
||||
int filled = 0;
|
||||
QHash<QString, int> seen_at;
|
||||
for (QDomElement t : terminals) {
|
||||
//Same count as Terminal::setPlaceRank(): every terminal before
|
||||
//this one at the same point, with or without a uuid
|
||||
const int rank = seen_at[terminalPlace(t)]++;
|
||||
if (!QUuid(t.attribute(QStringLiteral("uuid"))).isNull()) {
|
||||
continue;
|
||||
}
|
||||
const qreal x = t.attribute(QStringLiteral("x")).toDouble();
|
||||
const qreal y = t.attribute(QStringLiteral("y")).toDouble();
|
||||
const int orientation = orientationOf(t);
|
||||
QUuid uuid;
|
||||
for (int occurrence = rank ; uuid.isNull() || taken.contains(uuid) ; ++occurrence) {
|
||||
uuid = TerminalUuids::derived(x, y, orientation, occurrence);
|
||||
}
|
||||
taken << uuid;
|
||||
t.setAttribute(QStringLiteral("uuid"), uuid.toString());
|
||||
++filled;
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
int fillDirectory(const QDomElement &directory)
|
||||
{
|
||||
int filled = 0;
|
||||
for (QDomElement child = directory.firstChildElement();
|
||||
!child.isNull();
|
||||
child = child.nextSiblingElement()) {
|
||||
if (child.tagName() == QLatin1String("category")) {
|
||||
filled += fillDirectory(child);
|
||||
} else if (child.tagName() == QLatin1String("element")) {
|
||||
filled += fillDefinition(child);
|
||||
}
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,3 +206,49 @@ void TerminalUuids::keepInDirectory(const QDomElement &old_directory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::derived
|
||||
The identity of a terminal that carries no uuid, worked out from where
|
||||
it is inside its symbol: its local position and orientation, which are
|
||||
what the project file itself uses to tell terminals apart. UUID v5 in a
|
||||
fixed namespace, so the same terminal gets the same value in any
|
||||
project, on any machine, and it cannot collide with the random (v4)
|
||||
uuids the element editor gives.
|
||||
|
||||
@param orientation Qet::Orientation, as an int
|
||||
@param occurrence 0 for the value Terminal::stableUuid() uses; 1, 2...
|
||||
for a second, third... terminal at the same point of the same symbol
|
||||
*/
|
||||
QUuid TerminalUuids::derived(qreal x, qreal y, int orientation, int occurrence)
|
||||
{
|
||||
//Fixed namespace for terminal identities derived from geometry.
|
||||
static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
|
||||
QString key = QStringLiteral("%1|%2|%3")
|
||||
.arg(x, 0, 'f', 4)
|
||||
.arg(y, 0, 'f', 4)
|
||||
.arg(orientation);
|
||||
if (occurrence > 0) {
|
||||
key += QStringLiteral("|%1").arg(occurrence);
|
||||
}
|
||||
return QUuid::createUuidV5(derived_ns, key);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::fillMissing
|
||||
Give every terminal without a uuid, in every symbol of the embedded
|
||||
collection @p collection_root, the value derived() works out for it --
|
||||
the one QElectroTech already used for it as Terminal::stableUuid(), so
|
||||
nothing keyed on terminals changes. Saved with the project, it becomes
|
||||
a lasting identity: moving the terminal in the symbol editor no longer
|
||||
changes it.
|
||||
|
||||
Where one symbol has two terminals at one point, the second gets the
|
||||
next occurrence; a value is never given twice within a symbol.
|
||||
@return the number of terminals given a uuid
|
||||
*/
|
||||
int TerminalUuids::fillMissing(const QDomElement &collection_root)
|
||||
{
|
||||
return fillDirectory(collection_root);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@
|
||||
#ifndef TERMINALUUIDS_H
|
||||
#define TERMINALUUIDS_H
|
||||
|
||||
#include <QUuid>
|
||||
|
||||
class QDomElement;
|
||||
|
||||
namespace TerminalUuids
|
||||
{
|
||||
QUuid derived(qreal x, qreal y, int orientation, int occurrence = 0);
|
||||
int fillMissing(const QDomElement &collection_root);
|
||||
void keep(const QDomElement &old_element, QDomElement &new_element);
|
||||
void keepInDirectory(const QDomElement &old_directory,
|
||||
QDomElement &new_directory);
|
||||
|
||||
@@ -128,9 +128,13 @@ XmlElementCollection::XmlElementCollection(const QDomElement &dom_element,
|
||||
QObject(project),
|
||||
m_project(project)
|
||||
{
|
||||
if (dom_element.tagName() == "collection")
|
||||
if (dom_element.tagName() == "collection") {
|
||||
m_dom_document.appendChild(m_dom_document.importNode(
|
||||
dom_element, true));
|
||||
//Before any folio is built from these symbols, so that their
|
||||
//terminals carry the uuid the next save writes
|
||||
TerminalUuids::fillMissing(root());
|
||||
}
|
||||
else
|
||||
qDebug() << "XmlElementCollection : tagName of dom_element is not collection";
|
||||
}
|
||||
|
||||
@@ -1491,6 +1491,14 @@ Terminal* findTerminal(int conductor_index,
|
||||
continue;
|
||||
|
||||
return terminal;
|
||||
}
|
||||
//The uuid a project gave a terminal on opening is worked out
|
||||
//from where the terminal is in its symbol: if the symbol's
|
||||
//definition has since been replaced by one whose terminals
|
||||
//carry other uuids, the terminal at that place is still it.
|
||||
for (auto terminal: element->terminals()) {
|
||||
if (terminal->derivedUuid() == terminal_uuid)
|
||||
return terminal;
|
||||
}
|
||||
qDebug() << "Diagram::fromXml() : "
|
||||
<< terminal_index
|
||||
|
||||
@@ -697,6 +697,16 @@ Terminal *Element::parseTerminal(const QDomElement &dom_element)
|
||||
}
|
||||
|
||||
Terminal *new_terminal = new Terminal(data, this);
|
||||
//Terminals are parsed in the order of the definition, and the list
|
||||
//is not kept in that order (sort below)
|
||||
int rank = 0;
|
||||
for (Terminal *t : std::as_const(m_terminals)) {
|
||||
if (t->dock_elmt_ == new_terminal->dock_elmt_
|
||||
&& t->orientation() == new_terminal->orientation()) {
|
||||
++rank;
|
||||
}
|
||||
}
|
||||
new_terminal->setPlaceRank(rank);
|
||||
m_terminals << new_terminal;
|
||||
|
||||
connect(new_terminal, &Terminal::conductorWasAdded, this, &Element::updateConductorTexts);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../ElementsCollection/terminaluuids.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../conductorautonumerotation.h"
|
||||
@@ -849,11 +850,11 @@ QUuid Terminal::uuid() const
|
||||
read from the definition and is not touched by moving the element on the
|
||||
folio, so the result is stable across loads, saves and folio moves, and it
|
||||
is unique within an element except where a definition genuinely declares
|
||||
two terminals at the same point -- three cases in the whole example corpus,
|
||||
and harmless, because two terminals sharing a position and orientation are
|
||||
indistinguishable in every observable respect: they merge to one terminal
|
||||
row and every conductor on either of them still resolves to the right
|
||||
element and name.
|
||||
two terminals at the same point -- three cases in the whole example
|
||||
corpus. Opening a project gives every terminal of its symbols a uuid
|
||||
(TerminalUuids::fillMissing()), the same value for all but the second
|
||||
of such a pair, so in a project this is the fallback for terminals of a
|
||||
symbol imported since it was opened.
|
||||
|
||||
Derived values are UUID v5 in a fixed namespace, so they are reproducible
|
||||
without being written to the file, and cannot collide with the v4 uuids
|
||||
@@ -866,23 +867,45 @@ QUuid Terminal::stableUuid() const
|
||||
if (!d->m_uuid.isNull()) {
|
||||
return d->m_uuid;
|
||||
}
|
||||
return derivedUuid();
|
||||
}
|
||||
|
||||
//Fixed namespace for terminal identities derived from geometry.
|
||||
static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
/**
|
||||
@brief Terminal::derivedUuid
|
||||
The identity worked out from this terminal's local position and
|
||||
orientation, whether or not it carries a uuid of its own: the value
|
||||
stableUuid() gives when it has none, and the one a project gives it
|
||||
when it is opened (TerminalUuids::fillMissing()).
|
||||
|
||||
//Position and orientation only. The name is deliberately excluded: it
|
||||
//is not stable across a save cycle -- QET rewrites a terminal named
|
||||
//"_" as unnamed, which would silently change the identity of 1421 of
|
||||
//industrial.qet's 1790 terminals on the first resave. It is also not
|
||||
//needed: keying on geometry alone produces exactly the same number of
|
||||
//collisions across the example corpus, and it means renaming a
|
||||
//terminal does not change what it is.
|
||||
const QString key = QStringLiteral("%1|%2|%3")
|
||||
.arg(d->m_pos.x(), 0, 'f', 4)
|
||||
.arg(d->m_pos.y(), 0, 'f', 4)
|
||||
.arg(static_cast<int>(d->m_orientation));
|
||||
Position and orientation only. The name is deliberately excluded: it
|
||||
is not stable across a save cycle -- QET rewrites a terminal named "_"
|
||||
as unnamed, which would silently change the identity of 1421 of
|
||||
industrial.qet's 1790 terminals on the first resave. It is also not
|
||||
needed: keying on geometry alone produces exactly the same number of
|
||||
collisions across the example corpus, and it means renaming a terminal
|
||||
does not change what it is.
|
||||
|
||||
return QUuid::createUuidV5(derived_ns, key);
|
||||
A second terminal at the same point with the same orientation gets the
|
||||
next occurrence (see setPlaceRank()), as TerminalUuids::fillMissing()
|
||||
gives it, so the two terminals of such a pair are told apart.
|
||||
*/
|
||||
QUuid Terminal::derivedUuid() const
|
||||
{
|
||||
return TerminalUuids::derived(d->m_pos.x(),
|
||||
d->m_pos.y(),
|
||||
static_cast<int>(d->m_orientation),
|
||||
m_place_rank);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Terminal::setPlaceRank
|
||||
@param rank : how many terminals of the definition, before this one in
|
||||
document order, sit at the same point with the same orientation.
|
||||
Set by Element::parseTerminal().
|
||||
*/
|
||||
void Terminal::setPlaceRank(int rank)
|
||||
{
|
||||
m_place_rank = rank;
|
||||
}
|
||||
|
||||
QString Terminal::name() const
|
||||
|
||||
@@ -76,6 +76,8 @@ class Terminal : public QGraphicsObject
|
||||
Element *parentElement () const;
|
||||
QUuid uuid () const;
|
||||
QUuid stableUuid () const;
|
||||
QUuid derivedUuid () const;
|
||||
void setPlaceRank (int rank);
|
||||
QString name () const;
|
||||
QString baseName () const;
|
||||
TerminalData::Type terminalType() const;
|
||||
@@ -142,6 +144,9 @@ class Terminal : public QGraphicsObject
|
||||
Terminal *m_previous_terminal = nullptr;
|
||||
/// Whether the mouse pointer is hovering the terminal
|
||||
bool m_hovered = false;
|
||||
/// How many terminals of the definition, before this one, sit at
|
||||
/// the same point with the same orientation; see derivedUuid()
|
||||
int m_place_rank = 0;
|
||||
/// Color used for the hover effect
|
||||
QColor m_hovered_color = Terminal::neutralColor;
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ class tst_terminaluuids : public QObject
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
// --info on @p project in a sandbox of its own; returns the number of
|
||||
// wires loaded and puts what was written on stderr in @p log.
|
||||
int loadedWires(const QString &project, QString *log)
|
||||
// The real binary with @p args, in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead); false if it failed.
|
||||
bool runQet(const QStringList &args, QByteArray *out, QString *log)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
@@ -80,15 +80,51 @@ class tst_terminaluuids : public QObject
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--info"), project});
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args);
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return -1;
|
||||
return false;
|
||||
*log = QString::fromUtf8(proc.readAllStandardError());
|
||||
const QByteArray out = proc.readAllStandardOutput();
|
||||
*out = proc.readAllStandardOutput();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --info on @p project; returns the number of wires loaded and puts
|
||||
// what was written on stderr in @p log.
|
||||
int loadedWires(const QString &project, QString *log)
|
||||
{
|
||||
QByteArray out;
|
||||
if (!runQet({QStringLiteral("--info"), project}, &out, log))
|
||||
return -1;
|
||||
const QJsonDocument json = QJsonDocument::fromJson(out.mid(out.indexOf('{')));
|
||||
return json.object().value(QStringLiteral("conductors")).toInt(-1);
|
||||
}
|
||||
|
||||
// --resave @p in to a new file; returns its path, empty on failure.
|
||||
QString resave(const QString &in)
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("resaved%1.qet").arg(m_run));
|
||||
QByteArray stdout_;
|
||||
QString log;
|
||||
if (!runQet({QStringLiteral("--resave"), in, out}, &stdout_, &log))
|
||||
return {};
|
||||
return out;
|
||||
}
|
||||
|
||||
static QDomDocument load(const QString &path)
|
||||
{
|
||||
QDomDocument doc;
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
doc.setContent(&file);
|
||||
return doc;
|
||||
}
|
||||
|
||||
static QByteArray bytes(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
// 2612_ats_singlephase.qet with each embedded symbol replaced by a copy
|
||||
// whose terminals carry new uuids, passed through keep() or not.
|
||||
QString replacedSymbols(bool keep_uuids)
|
||||
@@ -221,6 +257,125 @@ private slots:
|
||||
QCOMPARE(uuids(deep), QStringList{"{b}"});
|
||||
}
|
||||
|
||||
// The recipe is Terminal::stableUuid()'s, which the project database
|
||||
// and the wire uuids worked out from their ends already use: changing
|
||||
// it would change every such identity in every project.
|
||||
void derivedRecipeUnchanged()
|
||||
{
|
||||
const QUuid ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
QCOMPARE(TerminalUuids::derived(0, 10, 2),
|
||||
QUuid::createUuidV5(ns, QStringLiteral("0.0000|10.0000|2")));
|
||||
QCOMPARE(TerminalUuids::derived(-2.5, 4, 3, 1),
|
||||
QUuid::createUuidV5(ns, QStringLiteral("-2.5000|4.0000|3|1")));
|
||||
}
|
||||
|
||||
// Only terminals without a uuid get one, the derived value; two at one
|
||||
// point get distinct ones; a value already used in the symbol is never
|
||||
// given again; symbols in sub-categories are reached.
|
||||
void fillMissing()
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement root = doc.createElement(QStringLiteral("collection"));
|
||||
QDomElement sub = doc.createElement(QStringLiteral("category"));
|
||||
root.appendChild(sub);
|
||||
const QString taken = TerminalUuids::derived(0, 0, 0).toString();
|
||||
const QString own = QStringLiteral("{0f5d4b0c-2f7e-4a55-9a51-8c3a3e1c2d11}");
|
||||
QDomElement a = symbol(doc, {{"0", "10", "s"},
|
||||
{"5", "0", "e", own},
|
||||
{"0", "10", "s"},
|
||||
{"0", "0", "n"},
|
||||
{"9", "9", "w", taken}});
|
||||
QDomElement b = symbol(doc, {{"1", "2", "w"}});
|
||||
root.appendChild(a);
|
||||
sub.appendChild(b);
|
||||
|
||||
QCOMPARE(TerminalUuids::fillMissing(root), 4);
|
||||
QCOMPARE(uuids(a), (QStringList{
|
||||
TerminalUuids::derived(0, 10, 2).toString(),
|
||||
own,
|
||||
TerminalUuids::derived(0, 10, 2, 1).toString(),
|
||||
TerminalUuids::derived(0, 0, 0, 1).toString(),
|
||||
taken}));
|
||||
QCOMPARE(uuids(b), QStringList{TerminalUuids::derived(1, 2, 3).toString()});
|
||||
QCOMPARE(TerminalUuids::fillMissing(root), 0);
|
||||
}
|
||||
|
||||
// An example whose symbols have no terminal uuids and whose wires are
|
||||
// all in the numbered form: once saved, every terminal has a uuid,
|
||||
// every wire names its ends by uuid, nothing is lost, and saving again
|
||||
// changes nothing.
|
||||
void resaveGivesEveryTerminalAUuid()
|
||||
{
|
||||
const QString original = QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet");
|
||||
QString log;
|
||||
const int wires = loadedWires(original, &log);
|
||||
QVERIFY(wires > 0);
|
||||
|
||||
const QString saved = resave(original);
|
||||
QVERIFY2(!saved.isEmpty(), "--resave failed");
|
||||
const QDomDocument doc = load(saved);
|
||||
int terminals = 0;
|
||||
for (const QDomElement &e : embeddedSymbols(doc)) {
|
||||
for (const QString &uuid : uuids(e)) {
|
||||
++terminals;
|
||||
QVERIFY2(!QUuid(uuid).isNull(), "a terminal has no uuid");
|
||||
}
|
||||
}
|
||||
QVERIFY(terminals > 0);
|
||||
const QDomNodeList conductors = doc.elementsByTagName(QStringLiteral("conductor"));
|
||||
QCOMPARE(conductors.size(), wires);
|
||||
for (int i = 0; i < conductors.size(); ++i) {
|
||||
const QDomElement c = conductors.at(i).toElement();
|
||||
QVERIFY2(c.hasAttribute(QStringLiteral("element1"))
|
||||
&& c.hasAttribute(QStringLiteral("element2")),
|
||||
"a wire is still in the numbered form");
|
||||
}
|
||||
|
||||
QCOMPARE(loadedWires(saved, &log), wires);
|
||||
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
|
||||
const QString again = resave(saved);
|
||||
QVERIFY2(!again.isEmpty(), "second --resave failed");
|
||||
QVERIFY2(bytes(again) == bytes(saved), "the second save changed the file");
|
||||
}
|
||||
|
||||
// The uuids written on opening are derived from where each terminal
|
||||
// is: a wire saved against one still finds its terminal after the
|
||||
// symbol's definition was replaced by one with other terminal uuids.
|
||||
// perceuse.qet and industrial.qet have wires on the second of two
|
||||
// terminals at one point of a symbol, which get the next occurrence.
|
||||
void derivedUuidFoundAfterReplacement_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
for (const char *name : {"tremie_vibrante.qet", "perceuse.qet", "industrial.qet"})
|
||||
QTest::newRow(name) << QStringLiteral(QET_EXAMPLES_DIR "/") + QLatin1String(name);
|
||||
}
|
||||
|
||||
void derivedUuidFoundAfterReplacement()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString saved = resave(project);
|
||||
QVERIFY(!saved.isEmpty());
|
||||
QString log;
|
||||
const int wires = loadedWires(saved, &log);
|
||||
QVERIFY(wires > 0);
|
||||
|
||||
QDomDocument doc = load(saved);
|
||||
for (QDomElement e : embeddedSymbols(doc)) {
|
||||
const QDomNodeList terminals = e.elementsByTagName(QStringLiteral("terminal"));
|
||||
for (int i = 0; i < terminals.size(); ++i)
|
||||
terminals.at(i).toElement().setAttribute(QStringLiteral("uuid"),
|
||||
QUuid::createUuid().toString());
|
||||
}
|
||||
const QString replaced = m_dir.filePath(QStringLiteral("replaced%1.qet").arg(m_run));
|
||||
QFile out(replaced);
|
||||
QVERIFY(out.open(QIODevice::WriteOnly));
|
||||
out.write(doc.toByteArray());
|
||||
out.close();
|
||||
|
||||
QCOMPARE(loadedWires(replaced, &log), wires);
|
||||
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
|
||||
}
|
||||
|
||||
// The real loader, on a project whose 131 wires are saved against
|
||||
// terminal uuids: replaced symbols lose 119 of them unless keep() ran,
|
||||
// and the ones lost are reported.
|
||||
|
||||
Reference in New Issue
Block a user