Files
qelectrotech-source-mirror/tests/qttest/tst_elementautonumids.cpp
T
2026-10-04 01:04:44 +02:00

1010 lines
44 KiB
C++

/*
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/>.
*/
// Element numbering schemes have an id, and elements follow a scheme by it
// (elementInformation "formula_id"). Runs the real binary on
// examples/industrial.qet, written before the ids existed: its schemes
// "Equipment" and "XV" are followed by elements, and it also holds
// elements whose formula no scheme defines any more (%prefixV1:%sequ_1...,
// left behind by earlier edits of a scheme), which must stay unlinked.
#include <QtTest>
#include <QDomDocument>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <QUuid>
class tst_elementautonumids : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
QString m_preferences; // contents of the settings file of the next run
QProcessEnvironment environment()
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
QDir().mkpath(home);
if (!m_preferences.isEmpty()) {
QDir().mkpath(home + QStringLiteral("/config/QElectroTech"));
QFile settings(home + QStringLiteral("/config/QElectroTech/QElectroTech.conf"));
if (settings.open(QIODevice::WriteOnly)) {
settings.write(m_preferences.toUtf8());
}
}
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
return env;
}
QString resave(const QString &in)
{
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
QProcess proc;
proc.setProcessEnvironment(environment());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
if (!proc.waitForFinished(180000) || proc.exitCode() != 0) return {};
return out;
}
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcess proc;
proc.setProcessEnvironment(environment());
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(180000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
qWarning().noquote() << out;
return {};
}
static QByteArray read(const QString &path)
{
QFile f(path);
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
}
struct Saved
{
QHash<QString, QUuid> scheme_id; // title -> id
QHash<QString, QString> scheme_formula; // title -> formula
QString current, current_id;
// per element: formula and formula_id
QList<QPair<QString, QString>> elements;
};
static Saved parse(const QByteArray &xml)
{
Saved s;
QDomDocument doc;
if (!doc.setContent(xml)) return s;
const QDomNodeList groups = doc.elementsByTagName(QStringLiteral("element_autonums"));
if (!groups.isEmpty()) {
const QDomElement g = groups.at(0).toElement();
s.current = g.attribute(QStringLiteral("current_autonum"));
s.current_id = g.attribute(QStringLiteral("current_autonum_id"));
const QDomNodeList schemes = g.elementsByTagName(QStringLiteral("element_autonum"));
for (int i = 0 ; i < schemes.count() ; ++i) {
const QDomElement e = schemes.at(i).toElement();
s.scheme_id.insert(e.attribute(QStringLiteral("title")),
QUuid(e.attribute(QStringLiteral("id"))));
s.scheme_formula.insert(e.attribute(QStringLiteral("title")),
e.attribute(QStringLiteral("formula")));
}
}
const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
for (int i = 0 ; i < elements.count() ; ++i) {
const QDomElement infos = elements.at(i).toElement()
.firstChildElement(QStringLiteral("elementInformations"));
if (infos.isNull()) continue;
QString formula, id;
for (QDomElement info = infos.firstChildElement(QStringLiteral("elementInformation"));
!info.isNull();
info = info.nextSiblingElement(QStringLiteral("elementInformation"))) {
if (info.attribute(QStringLiteral("name")) == QLatin1String("formula"))
formula = info.text();
else if (info.attribute(QStringLiteral("name")) == QLatin1String("formula_id"))
id = info.text();
}
s.elements << qMakePair(formula, id);
}
return s;
}
// industrial.qet with the element labelled @p label frozen
QString withFrozen(const QString &label)
{
QFile in(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
if (!in.open(QIODevice::ReadOnly)) return {};
QDomDocument doc;
if (!doc.setContent(&in)) return {};
int frozen = 0;
const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
for (int i = 0 ; i < elements.count() ; ++i) {
QDomElement e = elements.at(i).toElement();
const QDomElement infos = e.firstChildElement(QStringLiteral("elementInformations"));
for (QDomElement info = infos.firstChildElement(QStringLiteral("elementInformation"));
!info.isNull();
info = info.nextSiblingElement(QStringLiteral("elementInformation"))) {
if (info.attribute(QStringLiteral("name")) == QLatin1String("label")
&& info.text() == label) {
e.setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("true"));
++frozen;
}
}
}
if (frozen != 1) return {};
const QString out = m_dir.filePath(QStringLiteral("frozen%1.qet").arg(m_run));
QFile f(out);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(doc.toByteArray());
return out;
}
static QStringList diagramAttributes(const QByteArray &xml, const QString &attribute)
{
QStringList values;
QDomDocument doc;
if (!doc.setContent(xml)) return values;
const QDomNodeList diagrams = doc.elementsByTagName(QStringLiteral("diagram"));
for (int i = 0 ; i < diagrams.count() ; ++i)
values << diagrams.at(i).toElement().attribute(attribute);
return values;
}
// qet.autoNums() lists the numberings as "name: formula='...'"
static QStringList titlesOf(const QJsonValue &list)
{
QStringList titles;
for (const QJsonValue &entry : list.toArray()) {
const QString text = entry.toString();
const int cut = text.indexOf(QLatin1String(": formula="));
titles << (cut >= 0 ? text.left(cut) : text);
}
titles.sort();
return titles;
}
static int linkedTo(const Saved &s, const QString &title)
{
int n = 0;
for (const auto &e : s.elements)
if (QUuid(e.second) == s.scheme_id.value(title)) ++n;
return n;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
// A file without ids: every scheme gets one, the same on every load;
// an element is linked to the one scheme with its formula, and only then.
void legacyFileGetsIds()
{
const QString project = QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet");
const QString first = resave(project);
QVERIFY2(!first.isEmpty(), "--resave failed");
const QString again = resave(project);
QVERIFY2(!again.isEmpty(), "second --resave of the original failed");
//Two separate loads write attributes in their own order, so
//compare what they saved, not the bytes
const Saved s = parse(read(first));
const Saved s_again = parse(read(again));
QVERIFY2(s.scheme_id == s_again.scheme_id,
"the ids derived for a legacy file differ between loads");
QVERIFY2(s.elements == s_again.elements,
"the elements were linked differently on another load");
QVERIFY(!s.scheme_id.isEmpty());
QSet<QUuid> ids;
for (auto it = s.scheme_id.constBegin() ; it != s.scheme_id.constEnd() ; ++it) {
QVERIFY2(!it.value().isNull(), qPrintable(it.key() + QStringLiteral(" has no id")));
ids << it.value();
}
QCOMPARE(ids.size(), s.scheme_id.size());
QMultiHash<QString, QString> titles_by_formula;
for (auto it = s.scheme_formula.constBegin() ; it != s.scheme_formula.constEnd() ; ++it)
titles_by_formula.insert(it.value(), it.key());
for (const auto &e : s.elements) {
const QStringList titles = titles_by_formula.values(e.first);
if (!e.first.isEmpty() && titles.size() == 1) {
QCOMPARE(QUuid(e.second), s.scheme_id.value(titles.first()));
} else {
QVERIFY2(e.second.isEmpty(),
qPrintable(QStringLiteral("element with formula '%1' is linked")
.arg(e.first)));
}
}
QVERIFY(linkedTo(s, QStringLiteral("Equipment")) > 0);
QVERIFY(linkedTo(s, QStringLiteral("XV")) > 0);
const QString second = resave(first);
QVERIFY2(!second.isEmpty(), "--resave of the resaved file failed");
QVERIFY2(read(first) == read(second), "the second save changed the file");
}
// The id given to a numbering of a file written before the ids is the
// file's own: two projects which both have a numbering of the same name
// do not get the same id, or a paste from one into the other would
// take the other's numbering for the same one.
void derivedIdsBelongToTheirProject()
{
QFile in(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY(in.open(QIODevice::ReadOnly));
QDomDocument doc;
QVERIFY(doc.setContent(&in));
doc.documentElement().setAttribute(QStringLiteral("title"), QStringLiteral("Another project"));
const QString other = m_dir.filePath(QStringLiteral("another-project.qet"));
QFile out(other);
QVERIFY(out.open(QIODevice::WriteOnly));
out.write(doc.toByteArray());
out.close();
const QString first = resave(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
const QString second = resave(other);
QVERIFY2(!first.isEmpty() && !second.isEmpty(), "--resave failed");
const Saved a = parse(read(first));
const Saved b = parse(read(second));
QVERIFY(a.scheme_id.contains(QStringLiteral("Equipment")));
QVERIFY(b.scheme_id.contains(QStringLiteral("Equipment")));
QVERIFY(!a.scheme_id.value(QStringLiteral("Equipment")).isNull());
QVERIFY2(a.scheme_id.value(QStringLiteral("Equipment")) != b.scheme_id.value(QStringLiteral("Equipment")),
"two projects gave the same id to their numbering of the same name");
// each keeps its own ids when saved again
const QString again = resave(second);
QVERIFY(!again.isEmpty());
QCOMPARE(parse(read(again)).scheme_id, b.scheme_id);
}
// Rename, refused rename and removal, an edit that keeps the numbers,
// an edit that renumbers, and undo/redo: through the scripting API,
// which goes through the same ElementAutoNumSchemeCommand as the UI.
void renameEditRemoveUndo()
{
const QString saved = m_dir.filePath(QStringLiteral("scripted.qet"));
const QString script = QStringLiteral(R"JS(
function linked(formula) {
var r = [];
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'formula') === formula) r.push([f, u[i]]);
}
return r;
}
function label(e) { return qet.elementInfo(e[0], e[1], 'label'); }
function fid(e) { return qet.elementInfo(e[0], e[1], 'formula_id'); }
var xv = linked('%prefix2:%sequ_1');
var eq = linked('%id%prefix%sequ_1');
var r = {xv: xv.length, eq: eq.length};
var e0 = xv[0];
r.id0 = fid(e0);
r.label0 = label(e0);
r.renamed = qet.renameAutoNum('element', 'XV', 'Terminals XV');
r.idAfterRename = fid(e0);
r.clash = qet.renameAutoNum('element', 'Terminals XV', ' equipment ');
r.empty = qet.renameAutoNum('element', 'Terminals XV', ' ');
r.removeUsed = qet.removeAutoNum('element', 'Terminals XV');
r.removeUnused = qet.removeAutoNum('element', 'TB "XPE"');
r.edited = qet.addAutoNum('element', 'Terminals XV', ['elementprefix', 'string:X', 'unit:1']);
r.formulaAfterEdit = qet.elementInfo(e0[0], e0[1], 'formula');
r.labelAfterEdit = label(e0);
r.idAfterEdit = fid(e0);
qet.undo();
r.formulaAfterUndo = qet.elementInfo(e0[0], e0[1], 'formula');
r.labelAfterUndo = label(e0);
qet.redo();
r.labelAfterRedo = label(e0);
r.renumbered = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'string:-', 'ten:1']);
r.eqStillLinked = linked('%id%prefix-%seqt_1').length;
r.typed = qet.setElementInfo(eq[0][0], eq[0][1], 'formula', '%id%prefix%sequ_1');
r.typedId = fid(eq[0]);
r.internal = qet.setElementInfo(eq[1][0], eq[1][1], 'formula_id', '{00000000-0000-0000-0000-000000000001}');
r.names = qet.autoNums('element');
r.save = qet.save(%1);
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(QStringLiteral("'") + saved + QStringLiteral("'"));
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(r.value(QStringLiteral("xv")).toInt() > 0);
QVERIFY(r.value(QStringLiteral("eq")).toInt() > 1);
const QString id0 = r.value(QStringLiteral("id0")).toString();
QVERIFY(!QUuid(id0).isNull());
// a rename touches no element
QVERIFY(r.value(QStringLiteral("renamed")).toBool());
QCOMPARE(r.value(QStringLiteral("idAfterRename")).toString(), id0);
// names are unique ignoring case and surrounding spaces, never empty
QVERIFY(!r.value(QStringLiteral("clash")).toBool());
QVERIFY(!r.value(QStringLiteral("empty")).toBool());
// a scheme elements follow cannot be removed, an unused one can
QVERIFY(!r.value(QStringLiteral("removeUsed")).toBool());
QVERIFY(r.value(QStringLiteral("removeUnused")).toBool());
// same sequential parts: new formula, same number, still linked
QVERIFY(r.value(QStringLiteral("edited")).toBool());
QCOMPARE(r.value(QStringLiteral("formulaAfterEdit")).toString(),
QStringLiteral("%prefixX%sequ_1"));
QCOMPARE(r.value(QStringLiteral("idAfterEdit")).toString(), id0);
QString expected = r.value(QStringLiteral("label0")).toString();
const int colon = expected.lastIndexOf(QStringLiteral("2:"));
QVERIFY(colon >= 0);
expected.replace(colon, 2, QStringLiteral("X"));
QCOMPARE(r.value(QStringLiteral("labelAfterEdit")).toString(), expected);
// one undo step, both ways
QCOMPARE(r.value(QStringLiteral("formulaAfterUndo")).toString(),
QStringLiteral("%prefix2:%sequ_1"));
QCOMPARE(r.value(QStringLiteral("labelAfterUndo")).toString(),
r.value(QStringLiteral("label0")).toString());
QCOMPARE(r.value(QStringLiteral("labelAfterRedo")).toString(), expected);
// other sequential parts: renumbered, every element still linked
QVERIFY(r.value(QStringLiteral("renumbered")).toBool());
QCOMPARE(r.value(QStringLiteral("eqStillLinked")).toInt(),
r.value(QStringLiteral("eq")).toInt());
// a formula written by hand follows no scheme, formula_id is internal
QVERIFY(r.value(QStringLiteral("typed")).toBool());
QVERIFY(r.value(QStringLiteral("typedId")).toString().isEmpty());
QVERIFY(!r.value(QStringLiteral("internal")).toBool());
QVERIFY(r.value(QStringLiteral("save")).toBool());
const Saved s = parse(read(saved));
QVERIFY(s.scheme_id.contains(QStringLiteral("Terminals XV")));
QVERIFY(!s.scheme_id.contains(QStringLiteral("XV")));
QVERIFY(!s.scheme_id.contains(QStringLiteral("TB \"XPE\"")));
QCOMPARE(s.scheme_id.value(QStringLiteral("Terminals XV")), QUuid(id0));
QCOMPARE(linkedTo(s, QStringLiteral("Terminals XV")), r.value(QStringLiteral("xv")).toInt());
QCOMPARE(linkedTo(s, QStringLiteral("Equipment")), r.value(QStringLiteral("eq")).toInt() - 1);
}
// Giving a numbering to elements that have none (what picking it in the
// element's information window does): formula, id and the next number,
// the counter moving on, refused where something would be lost, and one
// undo step that gives the number back.
void assignScheme()
{
const QString script = QStringLiteral(R"JS(
function find(pred, max) {
var r = [];
for (var f = 0; f < qet.folioCount() && r.length < max; ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length && r.length < max; ++i)
if (pred(f, u[i])) r.push([f, u[i]]);
}
return r;
}
function info(e, k) { return qet.elementInfo(e[0], e[1], k); }
var r = {};
var candidates = find(function (f, u) { return qet.elementInfo(f, u, 'formula') === ''; }, 40);
var done = [];
for (var i = 0; i < candidates.length && done.length < 3; ++i)
if (qet.assignElementAutoNum('Equipment', candidates[i][0], candidates[i][1], false)) done.push(candidates[i]);
r.assigned = done.length;
var a = done[0], b = done[1], c = done[2];
r.formulaA = info(a, 'formula');
r.idA = info(a, 'formula_id');
r.labelA = info(a, 'label');
r.labelB = info(b, 'label');
r.labelC = info(c, 'label');
r.again = qet.assignElementAutoNum('Equipment', a[0], a[1], false);
r.other = qet.assignElementAutoNum('XV', a[0], a[1], false);
r.otherOverwrite = qet.assignElementAutoNum('XV', a[0], a[1], true);
r.formulaAfterOverwrite = info(a, 'formula');
r.idAfterOverwrite = info(a, 'formula_id');
r.unknown = qet.assignElementAutoNum('Nope', a[0], a[1], true);
qet.undo();
r.formulaAfterUndo = info(a, 'formula');
r.idAfterUndo = info(a, 'formula_id');
r.save = qet.save(%1);
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(QStringLiteral("'") + m_dir.filePath(QStringLiteral("assigned.qet")) + QStringLiteral("'"));
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value(QStringLiteral("assigned")).toInt(), 3);
QCOMPARE(r.value(QStringLiteral("formulaA")).toString(), QStringLiteral("%id%prefix%sequ_1"));
QVERIFY(!QUuid(r.value(QStringLiteral("idA")).toString()).isNull());
// the saved counter of "Equipment" is 9: the next numbers, in order
// (the label is the folio id, the prefix, then the number)
QVERIFY2(r.value(QStringLiteral("labelA")).toString().endsWith(QLatin1Char('9')),
qPrintable(r.value(QStringLiteral("labelA")).toString()));
QVERIFY2(r.value(QStringLiteral("labelB")).toString().endsWith(QStringLiteral("10")),
qPrintable(r.value(QStringLiteral("labelB")).toString()));
QVERIFY2(r.value(QStringLiteral("labelC")).toString().endsWith(QStringLiteral("11")),
qPrintable(r.value(QStringLiteral("labelC")).toString()));
QVERIFY(!r.value(QStringLiteral("again")).toBool());
QVERIFY(!r.value(QStringLiteral("other")).toBool());
QVERIFY(r.value(QStringLiteral("otherOverwrite")).toBool());
QCOMPARE(r.value(QStringLiteral("formulaAfterOverwrite")).toString(),
QStringLiteral("%prefix2:%sequ_1"));
QVERIFY(r.value(QStringLiteral("idAfterOverwrite")).toString()
!= r.value(QStringLiteral("idA")).toString());
QVERIFY(!r.value(QStringLiteral("unknown")).toBool());
// one undo gives the element its previous numbering back
QCOMPARE(r.value(QStringLiteral("formulaAfterUndo")).toString(),
QStringLiteral("%id%prefix%sequ_1"));
QCOMPARE(r.value(QStringLiteral("idAfterUndo")).toString(),
r.value(QStringLiteral("idA")).toString());
QVERIFY(r.value(QStringLiteral("save")).toBool());
const Saved saved = parse(read(m_dir.filePath(QStringLiteral("assigned.qet"))));
// three elements gained the numbering, whose counter moved past them
QVERIFY(linkedTo(saved, QStringLiteral("Equipment")) >= 3);
}
// An element whose label is frozen is left as it is by a renumbering, and
// the numbering goes past its number instead of giving it to another
// element. On industrial.qet the element labelled 5F3 is frozen: without
// that, the numbering would give 5F3 to another element of folio 5.
void frozenElementsAreSkipped()
{
const QString body = QStringLiteral(R"JS(
function labels() {
var r = {};
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1')
r[f + '/' + u[i]] = qet.elementInfo(f, u[i], 'label');
}
return r;
}
var before = labels();
var r = {before: before};
r.frozen = qet.renumberElementAutoNum('Equipment');
r.after = labels();
r.missing = qet.renumberElementAutoNum('Nope');
qet.undo();
r.undone = labels();
qet.log('PROBE ' + JSON.stringify(r));
)JS");
// control: nothing frozen, 5F3 goes to another element
const QJsonObject c = run(body, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!c.isEmpty(), "the control script logged nothing");
QCOMPARE(c.value(QStringLiteral("frozen")).toInt(), 0);
const QJsonObject cb = c.value(QStringLiteral("before")).toObject();
const QJsonObject ca = c.value(QStringLiteral("after")).toObject();
QString holder;
int given = 0;
for (auto it = cb.constBegin() ; it != cb.constEnd() ; ++it) {
if (it.value().toString() == QLatin1String("5F3")) holder = it.key();
}
QVERIFY2(!holder.isEmpty(), "no element is labelled 5F3 in industrial.qet");
for (auto it = ca.constBegin() ; it != ca.constEnd() ; ++it) {
if (it.value().toString() == QLatin1String("5F3") && it.key() != holder) ++given;
}
QVERIFY2(given == 1, "the control renumbering does not give 5F3 to another element: "
"this test would prove nothing");
// with the element frozen
const QString project = withFrozen(QStringLiteral("5F3"));
QVERIFY2(!project.isEmpty(), "cannot freeze the element");
const QJsonObject r = run(body, project);
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value(QStringLiteral("frozen")).toInt(), 1);
QCOMPARE(r.value(QStringLiteral("missing")).toInt(), -1);
const QJsonObject before = r.value(QStringLiteral("before")).toObject();
const QJsonObject after = r.value(QStringLiteral("after")).toObject();
QCOMPARE(after.size(), before.size());
QCOMPARE(after.value(holder).toString(), QStringLiteral("5F3"));
QSet<QString> seen;
int changed = 0;
for (auto it = after.constBegin() ; it != after.constEnd() ; ++it) {
const QString label = it.value().toString();
QVERIFY2(!seen.contains(label), qPrintable(QStringLiteral("two elements are labelled %1").arg(label)));
seen << label;
if (before.value(it.key()).toString() != label) ++changed;
}
QVERIFY(changed > 0); // the others were numbered again
// one undo gives every label back
QCOMPARE(r.value(QStringLiteral("undone")).toObject(), before);
}
// A scheme whose formula would change cannot be edited while an element
// with a frozen label follows it. A rename, and a change of the counter
// (same formula), still can: they touch no label.
void frozenElementsBlockAnEdit()
{
const QString project = withFrozen(QStringLiteral("5F3"));
QVERIFY2(!project.isEmpty(), "cannot freeze the element");
const QString script = QStringLiteral(R"JS(
function find(label) {
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
}
return null;
}
var frozen = find('5F3'), other = find('5F4');
var r = {oldFormula: qet.elementInfo(frozen[0], frozen[1], 'formula')};
r.refused = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:1', 'string:x']);
r.frozenFormula = qet.elementInfo(frozen[0], frozen[1], 'formula');
r.frozenLabel = qet.elementInfo(frozen[0], frozen[1], 'label');
r.otherFormula = qet.elementInfo(other[0], other[1], 'formula');
r.otherLabel = qet.elementInfo(other[0], other[1], 'label');
r.counter = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:40']);
r.otherAfterCounter = qet.elementInfo(other[0], other[1], 'label');
r.renamed = qet.renameAutoNum('element', 'Equipment', 'Equipment 2');
r.frozenId = qet.elementInfo(frozen[0], frozen[1], 'formula_id');
r.names = qet.autoNums('element');
qet.log('PROBE ' + JSON.stringify(r));
)JS");
const QJsonObject r = run(script, project);
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(!r.value(QStringLiteral("refused")).toBool());
QCOMPARE(r.value(QStringLiteral("frozenFormula")).toString(),
r.value(QStringLiteral("oldFormula")).toString());
QCOMPARE(r.value(QStringLiteral("frozenLabel")).toString(), QStringLiteral("5F3"));
QCOMPARE(r.value(QStringLiteral("otherFormula")).toString(), r.value(QStringLiteral("oldFormula")).toString());
QCOMPARE(r.value(QStringLiteral("otherLabel")).toString(), QStringLiteral("5F4"));
// same formula: allowed, and no label moves
QVERIFY(r.value(QStringLiteral("counter")).toBool());
QCOMPARE(r.value(QStringLiteral("otherAfterCounter")).toString(), QStringLiteral("5F4"));
// a rename is allowed, the frozen element still follows the numbering
QVERIFY(r.value(QStringLiteral("renamed")).toBool());
QVERIFY(!QUuid(r.value(QStringLiteral("frozenId")).toString()).isNull());
QVERIFY(QJsonDocument(r.value(QStringLiteral("names")).toArray()).toJson().contains("Equipment 2"));
}
// Duplicating elements which follow a numbering gives the copies the next
// numbers of it instead of the labels of the originals, and the numbering
// moves on; one undo takes the copies away and gives the numbers back, one
// redo brings the same numbers again.
void duplicatesAreNumbered()
{
const QString script = QStringLiteral(R"JS(
function labelsOf(f) {
var r = [], u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1') r.push(qet.elementInfo(f, u[i], 'label'));
return r;
}
function find(label) {
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
}
return null;
}
var a = find('8F1'), b = find('8F2');
var r = {before: labelsOf(a[0])};
var created = qet.duplicateElements(a[0], [a[1], b[1]], a[0], 0, 60);
r.count = created.length;
r.labels = created.map(function (u) { return qet.elementInfo(a[0], u, 'label'); });
r.formulas = created.map(function (u) { return qet.elementInfo(a[0], u, 'formula'); });
r.ids = created.map(function (u) { return qet.elementInfo(a[0], u, 'formula_id'); });
r.originalId = qet.elementInfo(a[0], a[1], 'formula_id');
r.afterAll = labelsOf(a[0]);
qet.undo();
r.afterUndo = labelsOf(a[0]);
qet.redo();
r.afterRedo = labelsOf(a[0]);
qet.undo();
// the counter went back: copying again gives the same numbers
var again = qet.duplicateElements(a[0], [a[1], b[1]], a[0], 0, 60);
r.again = again.map(function (u) { return qet.elementInfo(a[0], u, 'label'); });
qet.log('PROBE ' + JSON.stringify(r));
)JS");
m_preferences.clear();
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value(QStringLiteral("count")).toInt(), 2);
const QJsonArray labels = r.value(QStringLiteral("labels")).toArray();
const QJsonArray before = r.value(QStringLiteral("before")).toArray();
QCOMPARE(labels.size(), 2);
for (const QJsonValue &label : labels) {
QVERIFY2(!label.toString().isEmpty(), "a copy has no label");
QVERIFY2(!before.contains(label), qPrintable(QStringLiteral("a copy has the label %1 of another element").arg(label.toString())));
}
QVERIFY(labels.at(0) != labels.at(1));
QVERIFY(labels.at(0).toString().startsWith(QLatin1String("8F")));
const QJsonArray formulas = r.value(QStringLiteral("formulas")).toArray();
const QJsonArray ids = r.value(QStringLiteral("ids")).toArray();
for (int i = 0 ; i < 2 ; ++i) {
QCOMPARE(formulas.at(i).toString(), QStringLiteral("%id%prefix%sequ_1"));
QCOMPARE(ids.at(i).toString(), r.value(QStringLiteral("originalId")).toString());
}
// the originals are untouched, and every label is unique
const QJsonArray all = r.value(QStringLiteral("afterAll")).toArray();
QCOMPARE(all.size(), before.size() + 2);
QSet<QString> unique;
for (const QJsonValue &label : all) unique << label.toString();
QCOMPARE(unique.size(), all.size());
for (const QJsonValue &label : before) QVERIFY(all.contains(label));
// undo, redo, undo and copy again: the same numbers
QCOMPARE(r.value(QStringLiteral("afterUndo")).toArray(), before);
QCOMPARE(r.value(QStringLiteral("afterRedo")).toArray().size(), all.size());
QCOMPARE(r.value(QStringLiteral("again")).toArray(), labels);
}
// The preference "number pasted elements" is off: a copy is what a copy
// was, whatever the label preference says. Erasing (the default) leaves
// it with no label and no numbering; keeping leaves it with the label of
// the original, still tied to the same numbering.
void duplicatesAreNotNumberedWhenSwitchedOff()
{
const QString script = QStringLiteral(R"JS(
function find(label) {
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
}
return null;
}
var a = find('8F1');
var created = qet.duplicateElements(a[0], [a[1]], a[0], 0, 60);
var r = {label: qet.elementInfo(a[0], created[0], 'label'),
formula: qet.elementInfo(a[0], created[0], 'formula'),
id: qet.elementInfo(a[0], created[0], 'formula_id')};
qet.log('PROBE ' + JSON.stringify(r));
)JS");
m_preferences = QStringLiteral("[diagramcommands]\nautonumber-pasted-elements=false\n");
const QJsonObject erased = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!erased.isEmpty(), "the script logged nothing");
QVERIFY(erased.value(QStringLiteral("label")).toString().isEmpty());
QVERIFY(erased.value(QStringLiteral("formula")).toString().isEmpty());
QVERIFY(erased.value(QStringLiteral("id")).toString().isEmpty());
m_preferences = QStringLiteral("[diagramcommands]\nautonumber-pasted-elements=false\nerase-label-on-copy=false\n");
const QJsonObject kept = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!kept.isEmpty(), "the script logged nothing");
QCOMPARE(kept.value(QStringLiteral("label")).toString(), QStringLiteral("8F1"));
QCOMPARE(kept.value(QStringLiteral("formula")).toString(), QStringLiteral("%id%prefix%sequ_1"));
QVERIFY(!QUuid(kept.value(QStringLiteral("id")).toString()).isNull());
m_preferences.clear();
}
// Conductor numberings: names are unique and never empty, a rename moves
// the folios which read the numbering with it (26 folios of
// industrial.qet read "Wire"), a numbering which folios read cannot be
// removed, an unused one can, and every step is one undo.
void conductorNumberings()
{
const QString saved = m_dir.filePath(QStringLiteral("conductors.qet"));
const QString script = QStringLiteral(R"JS(
var r = {};
r.names = qet.autoNums('conductor');
r.clash = qet.addAutoNum('conductor', ' wire ', ['unit:1']);
r.empty = qet.addAutoNum('conductor', ' ', ['unit:1']);
r.added = qet.addAutoNum('conductor', 'Spare', ['string:S', 'unit:1']);
r.renameClash = qet.renameAutoNum('conductor', 'Spare', 'WIRE');
r.removeUsed = qet.removeAutoNum('conductor', 'Wire');
r.renamed = qet.renameAutoNum('conductor', 'Wire', 'Cable');
r.afterRename = qet.autoNums('conductor');
r.missing = qet.renameAutoNum('conductor', 'Nope', 'X');
r.removeSpare = qet.removeAutoNum('conductor', 'Spare');
r.afterRemove = qet.autoNums('conductor');
qet.undo(); // the removal
r.afterUndoRemove = qet.autoNums('conductor');
qet.undo(); qet.undo(); // the rename, then the creation
qet.redo(); qet.redo(); // the creation, then the rename
r.afterRedo = qet.autoNums('conductor');
r.save = qet.save(%1);
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(QStringLiteral("'") + saved + QStringLiteral("'"));
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const auto names = [&](const char *key) {
return titlesOf(r.value(QLatin1String(key)));
};
QCOMPARE(names("names"), QStringList{QStringLiteral("Wire")});
QVERIFY(!r.value(QStringLiteral("clash")).toBool()); // " wire " is "Wire"
QVERIFY(!r.value(QStringLiteral("empty")).toBool());
QVERIFY(r.value(QStringLiteral("added")).toBool());
QVERIFY(!r.value(QStringLiteral("renameClash")).toBool()); // "WIRE" is "Wire"
QVERIFY(!r.value(QStringLiteral("removeUsed")).toBool()); // 26 folios read it
QVERIFY(r.value(QStringLiteral("renamed")).toBool());
QCOMPARE(names("afterRename"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
QVERIFY(!r.value(QStringLiteral("missing")).toBool());
QVERIFY(r.value(QStringLiteral("removeSpare")).toBool()); // no folio reads it
QCOMPARE(names("afterRemove"), QStringList{QStringLiteral("Cable")});
QCOMPARE(names("afterUndoRemove"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
QCOMPARE(names("afterRedo"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
// the folios read the numbering under its new name
QVERIFY(r.value(QStringLiteral("save")).toBool());
const QByteArray xml = read(saved);
const QStringList followed = diagramAttributes(xml, QStringLiteral("conductorAutonum"));
QCOMPARE(followed.count(QStringLiteral("Cable")), 26);
QCOMPARE(followed.count(QStringLiteral("Wire")), 0);
}
// Folio numberings. A folio numbering is applied once: the number is
// written into the folio field and the title block keeps the name (49 of
// the 50 folios of industrial.qet name "Name of the new numbering", none
// has a folio field which still holds %autonum). A rename moves every name,
// removing a numbering which folios only name is allowed, and undo and
// redo give back the same names.
void folioNumberings()
{
const QString renamed_file = m_dir.filePath(QStringLiteral("folio-renamed.qet"));
const QString undone_file = m_dir.filePath(QStringLiteral("folio-undone.qet"));
const QString script = QStringLiteral(R"JS(
var r = {};
r.names = qet.autoNums('folio');
r.clash = qet.addAutoNum('folio', 'name of the new numbering', ['unit:1']);
r.empty = qet.addAutoNum('folio', ' ', ['unit:1']);
r.renamed = qet.renameAutoNum('folio', 'Name of the new numbering', 'Folios');
r.afterRename = qet.autoNums('folio');
r.saveRenamed = qet.save(%1);
r.removed = qet.removeAutoNum('folio', 'Folios');
r.afterRemove = qet.autoNums('folio');
qet.undo(); // the removal
r.afterUndoRemove = qet.autoNums('folio');
qet.undo(); // the rename
r.afterUndoRename = qet.autoNums('folio');
r.saveUndone = qet.save(%2);
qet.redo();
r.afterRedo = qet.autoNums('folio');
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(QStringLiteral("'") + renamed_file + QStringLiteral("'"),
QStringLiteral("'") + undone_file + QStringLiteral("'"));
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const auto names = [&](const char *key) {
return titlesOf(r.value(QLatin1String(key)));
};
const QString old_name = QStringLiteral("Name of the new numbering");
QCOMPARE(names("names"), QStringList{old_name});
QVERIFY(!r.value(QStringLiteral("clash")).toBool());
QVERIFY(!r.value(QStringLiteral("empty")).toBool());
QVERIFY(r.value(QStringLiteral("renamed")).toBool());
QCOMPARE(names("afterRename"), QStringList{QStringLiteral("Folios")});
QVERIFY(r.value(QStringLiteral("removed")).toBool());
QCOMPARE(names("afterRemove"), QStringList());
QCOMPARE(names("afterUndoRemove"), QStringList{QStringLiteral("Folios")});
QCOMPARE(names("afterUndoRename"), QStringList{old_name});
QCOMPARE(names("afterRedo"), QStringList{QStringLiteral("Folios")});
QVERIFY(r.value(QStringLiteral("saveRenamed")).toBool());
QStringList named = diagramAttributes(read(renamed_file), QStringLiteral("auto_page_num"));
QCOMPARE(named.size(), 50);
QCOMPARE(named.count(QStringLiteral("Folios")), 49);
QCOMPARE(named.count(old_name), 0);
QVERIFY(r.value(QStringLiteral("saveUndone")).toBool());
named = diagramAttributes(read(undone_file), QStringLiteral("auto_page_num"));
QCOMPARE(named.count(old_name), 49);
QCOMPARE(named.count(QStringLiteral("Folios")), 0);
}
// Free numbers. An element which follows a numbering of one sequence may be
// given a number nobody has, and keeps following the numbering: the case of
// an element which, after a reshuffle, must have its real-life number back.
// Only free numbers are offered and accepted; one undo gives the number back;
// a number past the counter moves the counter, so that the next element is
// not given the same label.
void freeNumbers()
{
const QString script = QStringLiteral(R"JS(
function find(label) {
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
}
return null;
}
function number(label) { var m = /(\d+)$/.exec(label); return m ? parseInt(m[1]) : -1; }
function followers() {
var r = [];
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1') r.push(number(qet.elementInfo(f, u[i], 'label')));
}
return r.sort(function (a, b) { return a - b; });
}
var e = find('8F2'), other = find('8F1');
var r = {used: followers()};
r.before = qet.elementInfo(e[0], e[1], 'label');
r.own = number(r.before);
r.free = qet.freeElementNumbers(e[0], e[1]);
r.numberOfOther = number(qet.elementInfo(other[0], other[1], 'label'));
r.idBefore = qet.elementInfo(e[0], e[1], 'formula_id');
r.assignUsed = qet.assignElementNumber(e[0], e[1], r.numberOfOther); // taken: refused
r.assignZero = qet.assignElementNumber(e[0], e[1], 0);
r.assignFree = qet.assignElementNumber(e[0], e[1], r.free[0]);
r.after = qet.elementInfo(e[0], e[1], 'label');
r.formulaAfter = qet.elementInfo(e[0], e[1], 'formula');
r.idAfter = qet.elementInfo(e[0], e[1], 'formula_id');
r.freeAfter = qet.freeElementNumbers(e[0], e[1]); // its old number is free now
qet.undo();
r.undone = qet.elementInfo(e[0], e[1], 'label');
// a number past the counter: the counter moves past it
var high = r.free[r.free.length - 1];
r.high = high;
r.assignHigh = qet.assignElementNumber(e[0], e[1], high);
r.highLabel = qet.elementInfo(e[0], e[1], 'label');
var plain = null;
for (var f = 0; f < qet.folioCount() && !plain; ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i)
if (qet.elementInfo(f, u[i], 'formula') === '') { plain = [f, u[i]]; break; }
}
// an element which follows nothing has no numbers to choose from, nor can it be given one
r.noFree = qet.freeElementNumbers(plain[0], plain[1]).length;
r.assignNothing = qet.assignElementNumber(plain[0], plain[1], 5);
// the next element of the numbering is past the number given by hand
r.assignNext = qet.assignElementAutoNum('Equipment', plain[0], plain[1], false);
r.nextNumber = number(qet.elementInfo(plain[0], plain[1], 'label'));
qet.log('PROBE ' + JSON.stringify(r));
)JS");
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
const QJsonArray used = r.value(QStringLiteral("used")).toArray();
const QJsonArray free = r.value(QStringLiteral("free")).toArray();
QVERIFY(!free.isEmpty());
const int own = r.value(QStringLiteral("own")).toInt();
for (const QJsonValue &n : free)
QVERIFY2(!used.contains(n) || n.toInt() == own, "a number in use is offered");
QVERIFY(!free.contains(r.value(QStringLiteral("numberOfOther"))));
QVERIFY(!r.value(QStringLiteral("assignUsed")).toBool());
QVERIFY(!r.value(QStringLiteral("assignZero")).toBool());
QVERIFY(r.value(QStringLiteral("assignFree")).toBool());
const QString after = r.value(QStringLiteral("after")).toString();
QVERIFY(after != r.value(QStringLiteral("before")).toString());
QVERIFY2(after.endsWith(QString::number(free.at(0).toInt())), qPrintable(after));
QVERIFY(after.startsWith(QLatin1String("8F")));
QCOMPARE(r.value(QStringLiteral("formulaAfter")).toString(), QStringLiteral("%id%prefix%sequ_1"));
QCOMPARE(r.value(QStringLiteral("idAfter")).toString(), r.value(QStringLiteral("idBefore")).toString());
// the number it had is free for another element now
QVERIFY(r.value(QStringLiteral("freeAfter")).toArray().size() >= free.size());
QCOMPARE(r.value(QStringLiteral("undone")).toString(), r.value(QStringLiteral("before")).toString());
// a number past the counter moves the counter on
QVERIFY(r.value(QStringLiteral("assignHigh")).toBool());
QVERIFY(r.value(QStringLiteral("assignNext")).toBool());
QVERIFY2(r.value(QStringLiteral("nextNumber")).toInt() > r.value(QStringLiteral("high")).toInt(),
"the next element was given a number which is not past the one given by hand");
QCOMPARE(r.value(QStringLiteral("noFree")).toInt(), 0);
QVERIFY(!r.value(QStringLiteral("assignNothing")).toBool());
}
// A numbering whose counter is set back over numbers in use does not hand
// them out again: placing an element takes the first number whose label
// nobody carries, and the counter goes on from there; one undo gives the
// counter back. A label held by an element which does not follow the
// numbering counts too.
void placementSkipsLabelsInUse()
{
const QString script = QStringLiteral(R"JS(
function labels() {
var r = [];
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i) {
var l = qet.elementInfo(f, u[i], 'label');
if (l !== '') r.push(l);
}
}
return r;
}
// number the first element without a formula which accepts one (a slave or a report does not)
function numberOne(skip) {
for (var f = 0; f < qet.folioCount(); ++f) {
var u = qet.elementUuids(f);
for (var i = 0; i < u.length; ++i) {
if (qet.elementInfo(f, u[i], 'formula') !== '' || skip.indexOf(u[i]) >= 0) continue;
if (qet.numberElement(f, u[i])) return [f, u[i]];
}
}
return null;
}
var r = {};
// the counter of "Equipment" back at 1, over the 29 elements which have numbers: allowed
r.back = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:1']);
r.use = qet.useElementAutoNum('Equipment');
var held = labels();
var a = numberOne([]);
r.placedA = a !== null;
r.labelA = qet.elementInfo(a[0], a[1], 'label');
r.heldA = held.indexOf(r.labelA) >= 0;
var b = numberOne([a[1]]);
r.placedB = b !== null;
r.labelB = qet.elementInfo(b[0], b[1], 'label');
r.heldB = held.indexOf(r.labelB) >= 0 || r.labelB === r.labelA;
r.formulaA = qet.elementInfo(a[0], a[1], 'formula');
// one undo takes B's number back, the next undo A's
qet.undo();
r.afterUndoB = qet.elementInfo(b[0], b[1], 'label');
qet.undo();
r.afterUndoA = qet.elementInfo(a[0], a[1], 'label');
r.again = qet.numberElement(a[0], a[1]);
r.labelAgain = qet.elementInfo(a[0], a[1], 'label');
qet.log('PROBE ' + JSON.stringify(r));
)JS");
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QVERIFY(r.value(QStringLiteral("back")).toBool());
QVERIFY(r.value(QStringLiteral("use")).toBool());
QVERIFY(r.value(QStringLiteral("placedA")).toBool());
QVERIFY(r.value(QStringLiteral("placedB")).toBool());
const QString a = r.value(QStringLiteral("labelA")).toString();
const QString b = r.value(QStringLiteral("labelB")).toString();
QVERIFY2(!a.isEmpty() && !b.isEmpty(), "an element was not given a label");
QVERIFY2(!r.value(QStringLiteral("heldA")).toBool(), qPrintable(QStringLiteral("%1 is held by another element").arg(a)));
QVERIFY2(!r.value(QStringLiteral("heldB")).toBool(), qPrintable(QStringLiteral("%1 is held by another element").arg(b)));
QVERIFY(a != b);
QCOMPARE(r.value(QStringLiteral("formulaA")).toString(), QStringLiteral("%id%prefix%sequ_1"));
// undoing gives the numbers back, and numbering again gives the same label
QVERIFY(r.value(QStringLiteral("afterUndoB")).toString() != b);
QVERIFY(r.value(QStringLiteral("afterUndoA")).toString() != a);
QVERIFY(r.value(QStringLiteral("again")).toBool());
QCOMPARE(r.value(QStringLiteral("labelAgain")).toString(), a);
}
};
QTEST_APPLESS_MAIN(tst_elementautonumids)
#include "tst_elementautonumids.moc"