mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-19 20:44:16 +02:00
Merge master into fix/joined-wire-uuid
The conflict was tests/qttest/CMakeLists.txt, where #1520 added tst_savedstamps at the place this branch adds tst_joinedwireuuid. Both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -889,6 +889,18 @@ if(QET_HAS_SCRIPTING)
|
||||
target_compile_definitions(tst_scriptconductoruuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The saved date, time and path of the title block change only when the
|
||||
# project changed, and go into the file that save writes (F124): an
|
||||
# unchanged project saved twice gives an identical file.
|
||||
add_executable(
|
||||
tst_savedstamps
|
||||
tst_savedstamps.cpp)
|
||||
add_test(NAME tst_savedstamps COMMAND tst_savedstamps)
|
||||
add_dependencies(tst_savedstamps qelectrotech)
|
||||
target_link_libraries(tst_savedstamps PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_savedstamps PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# Deleting a symbol whose terminal held several wires joins them with a
|
||||
# new wire; its uuid comes from its two ends, so the same deletion saves
|
||||
# the same file every time (F115): examples/affuteuse_250h.qet.
|
||||
|
||||
@@ -214,7 +214,9 @@ class tst_projectlock : public QObject
|
||||
|
||||
void readOnlyFolderDoesNotBlock()
|
||||
{
|
||||
#ifndef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN
|
||||
QSKIP("a read-only folder still takes new files on Windows");
|
||||
#else
|
||||
if (geteuid() == 0)
|
||||
QSKIP("root can write anywhere");
|
||||
#endif
|
||||
|
||||
@@ -69,10 +69,12 @@ private slots:
|
||||
f.close();
|
||||
|
||||
// Columns numbered from 1, as QElectroTech's settings page
|
||||
// shows by default
|
||||
// shows by default. Written to an INI file QET_SETTINGS_DIR
|
||||
// points at: on Windows the settings are in the registry
|
||||
// otherwise, and the columns would count from 0
|
||||
const QString home = m_dir.filePath(tag + QStringLiteral("-home"));
|
||||
QVERIFY(QDir().mkpath(home + QStringLiteral("/config/QElectroTech")));
|
||||
QFile conf(home + QStringLiteral("/config/QElectroTech/QElectroTech.conf"));
|
||||
QVERIFY(QDir().mkpath(home + QStringLiteral("/settings/QElectroTech")));
|
||||
QFile conf(home + QStringLiteral("/settings/QElectroTech/QElectroTech.ini"));
|
||||
QVERIFY(conf.open(QIODevice::WriteOnly));
|
||||
conf.write("[General]\nborder-columns_0=false\n");
|
||||
conf.close();
|
||||
@@ -80,6 +82,7 @@ private slots:
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("QET_SETTINGS_DIR"), home + QStringLiteral("/settings"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
|
||||
@@ -411,14 +411,15 @@ private slots:
|
||||
|
||||
// QElectroTech run in a sandbox of its own with @p args, the store
|
||||
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output
|
||||
QString runChecked(const QStringList &args, bool eager = false)
|
||||
QString runChecked(const QStringList &args, bool eager = false,
|
||||
const char *check = "QET_CHECK_ELEMENT_INFO_STORE")
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
|
||||
env.insert(QString::fromLatin1(check), QStringLiteral("1"));
|
||||
if (eager) {
|
||||
env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening
|
||||
}
|
||||
@@ -452,6 +453,31 @@ private slots:
|
||||
QVERIFY2(out.contains(QStringLiteral("wire properties store: 0 differ")), qPrintable(out.right(600)));
|
||||
}
|
||||
|
||||
// The link rows say what each symbol saves, in its order, also after
|
||||
// an edit builds a partner's folio (lazy folios): m_000 as a .qetz
|
||||
// saved with every folio built, a symbol of its middle folio moved. The rows used to follow the order
|
||||
// the partners were built in (QETZ-DB-FIRST stage 2a).
|
||||
void linkRowsAfterLazyEdit()
|
||||
{
|
||||
const QString qetz = m_dir.filePath(QStringLiteral("links%1.qetz").arg(m_run));
|
||||
QVERIFY(!runChecked({QStringLiteral("--resave"),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"), qetz}, true).isEmpty());
|
||||
const QString saved = m_dir.filePath(QStringLiteral("links-moved%1.qetz").arg(m_run));
|
||||
const QString js = m_dir.filePath(QStringLiteral("links%1.js").arg(m_run));
|
||||
QFile script(js);
|
||||
QVERIFY(script.open(QIODevice::WriteOnly));
|
||||
script.write(QStringLiteral(
|
||||
"var f = Math.floor(qet.folioCount() / 2);\n"
|
||||
"var u = qet.elementUuids(f).slice().sort()[0];\n"
|
||||
"qet.log('MOVED ' + qet.moveElement(f, u, 0, 100));\n"
|
||||
"qet.save('%1');\n").arg(saved).toUtf8());
|
||||
script.close();
|
||||
const QString out = runChecked({QStringLiteral("--run"), js, qetz}, false,
|
||||
"QET_CHECK_LINK_ROWS");
|
||||
QVERIFY2(out.contains(QStringLiteral("MOVED true")), qPrintable(out.right(600)));
|
||||
QVERIFY2(out.contains(QStringLiteral("link rows: 0 differ")), qPrintable(out.right(600)));
|
||||
}
|
||||
|
||||
// The stores filled from the document before the folios are built
|
||||
// hold what building them gives, for a
|
||||
// project saved by this version (every symbol, folio and wire carries
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QDate>
|
||||
#include <QDomDocument>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
The title block variables of a save (saveddate, savedtime,
|
||||
savedfilename, savedfilepath...) are set when the project is saved
|
||||
with something new in it, and go into the file that save writes. They
|
||||
used to be set after writing, so each save wrote the time of the one
|
||||
before: a project saved twice without a change gave two different
|
||||
files, and a changed project was saved with the previous date.
|
||||
|
||||
Runs the real binary on fixtures/unlinked_contact_label.qet, whose
|
||||
stamps say 2026-10-02 18:39 in a temporary folder:
|
||||
- saved twice in its own file without a change: byte-identical;
|
||||
- one symbol moved, then saved: today's date and the file's own path.
|
||||
*/
|
||||
class tst_savedstamps : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
QString m_base;
|
||||
|
||||
QString run(const QStringList &arguments)
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
//Its own settings and single-instance socket: another test's
|
||||
//QElectroTech running at the same time would otherwise be
|
||||
//handed this run (SingleApplication)
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
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());
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.setProcessChannelMode(QProcess::MergedChannels);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
if (!proc.waitForFinished(120000) || proc.exitStatus() != QProcess::NormalExit
|
||||
|| proc.exitCode() != 0)
|
||||
return QString();
|
||||
return QString::fromUtf8(proc.readAll()) + QStringLiteral("\nEXIT 0");
|
||||
}
|
||||
|
||||
QString script(const QString &name, const QString &body)
|
||||
{
|
||||
const QString path = m_dir.filePath(name);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) return QString();
|
||||
file.write(body.toUtf8());
|
||||
return path;
|
||||
}
|
||||
|
||||
/// The saved-* properties of @p qetz, read back through --resave
|
||||
QMap<QString, QString> stamps(const QString &qetz)
|
||||
{
|
||||
const QString xml = qetz + QStringLiteral(".qet");
|
||||
if (run({QStringLiteral("--resave"), qetz, xml}).isEmpty()) return {};
|
||||
QFile file(xml);
|
||||
QDomDocument doc;
|
||||
if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return {};
|
||||
QMap<QString, QString> out;
|
||||
const QDomNodeList props = doc.documentElement().firstChildElement(
|
||||
QStringLiteral("properties")).elementsByTagName(QStringLiteral("property"));
|
||||
for (int i = 0; i < props.size(); ++i) {
|
||||
const QDomElement p = props.at(i).toElement();
|
||||
if (p.attribute(QStringLiteral("name")).startsWith(QLatin1String("saved")))
|
||||
out.insert(p.attribute(QStringLiteral("name")), p.text());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static QByteArray bytes(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
|
||||
QVERIFY(m_dir.isValid());
|
||||
const QString fixture = QFINDTESTDATA("fixtures/unlinked_contact_label.qet");
|
||||
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||
//The fixture as a .qetz, its stamps as they were: a copy
|
||||
//written by --resave is not a save of the project
|
||||
m_base = m_dir.filePath(QStringLiteral("base.qetz"));
|
||||
QVERIFY(!run({QStringLiteral("--resave"), fixture, m_base}).isEmpty());
|
||||
QCOMPARE(stamps(m_base).value(QStringLiteral("saveddate-us")), QStringLiteral("2026-10-02"));
|
||||
}
|
||||
|
||||
void unchangedSavesAreIdentical()
|
||||
{
|
||||
const QString project = m_dir.filePath(QStringLiteral("unchanged.qetz"));
|
||||
QVERIFY(QFile::copy(m_base, project));
|
||||
const QString js = script(QStringLiteral("unchanged.js"),
|
||||
QStringLiteral("qet.save('');\nqet.save('');\nqet.log('SAVED twice');\n"));
|
||||
QVERIFY(run({QStringLiteral("--run"), js, project}).contains(QLatin1String("SAVED twice")));
|
||||
QCOMPARE(bytes(project), bytes(m_base));
|
||||
}
|
||||
|
||||
void changedSaveIsStampedNow()
|
||||
{
|
||||
const QString project = m_dir.filePath(QStringLiteral("changed.qetz"));
|
||||
QVERIFY(QFile::copy(m_base, project));
|
||||
const QString js = script(QStringLiteral("changed.js"), QStringLiteral(
|
||||
"var u = qet.elementUuids(0)[0];\n"
|
||||
"qet.log('MOVED ' + qet.moveElement(0, u, 10, 0));\n"
|
||||
"qet.save('');\n"));
|
||||
QVERIFY(run({QStringLiteral("--run"), js, project}).contains(QLatin1String("MOVED true")));
|
||||
const QMap<QString, QString> s = stamps(project);
|
||||
QCOMPARE(s.value(QStringLiteral("saveddate-us")), QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd")));
|
||||
QCOMPARE(s.value(QStringLiteral("savedfilepath")), QFileInfo(project).absoluteFilePath());
|
||||
QCOMPARE(s.value(QStringLiteral("savedfilename")), QStringLiteral("changed"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_savedstamps)
|
||||
#include "tst_savedstamps.moc"
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
#include <QSet>
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
@@ -150,13 +151,66 @@ private slots:
|
||||
QVERIFY(!twice.isEmpty());
|
||||
QCOMPARE(nets(twice), expected);
|
||||
|
||||
//No wire names a lamp by the uuid they share
|
||||
for (const QDomElement &c : childElements(firstDiagram(load(twice)),
|
||||
QStringLiteral("conductors"),
|
||||
QStringLiteral("conductor"))) {
|
||||
QVERIFY(c.attribute(QStringLiteral("element1")) != shared);
|
||||
QVERIFY(c.attribute(QStringLiteral("element2")) != shared);
|
||||
//Each lamp has a uuid of its own after a save: one keeps the
|
||||
//shared uuid, the others get one derived from it
|
||||
QSet<QString> uuids;
|
||||
for (const QDomElement &e : lamps(load(twice)))
|
||||
uuids.insert(e.attribute(QStringLiteral("uuid")));
|
||||
QCOMPARE(uuids.size(), 3);
|
||||
QVERIFY(uuids.contains(shared));
|
||||
}
|
||||
|
||||
// A linked contact and an unlinked symbol carrying the same uuid (a copy
|
||||
// made by an older version): the linked one keeps the uuid, the other
|
||||
// gets its own, and every link and wire stays where it was.
|
||||
void linkedCopyKeepsUuid()
|
||||
{
|
||||
const QString source = QStringLiteral(QET_EXAMPLES_DIR "/affuteuse_250h.qet");
|
||||
QDomDocument doc = load(source);
|
||||
QList<QDomElement> all;
|
||||
const QDomNodeList folios = doc.documentElement().elementsByTagName(QStringLiteral("diagram"));
|
||||
for (int i = 0; i < folios.size(); ++i)
|
||||
all << childElements(folios.at(i).toElement(), QStringLiteral("elements"),
|
||||
QStringLiteral("element"));
|
||||
auto linked = [](const QDomElement &e) {
|
||||
return !e.firstChildElement(QStringLiteral("links_uuids"))
|
||||
.firstChildElement(QStringLiteral("link_uuid")).isNull();
|
||||
};
|
||||
QDomElement keeper, copy;
|
||||
for (const QDomElement &e : std::as_const(all)) {
|
||||
if (keeper.isNull() && linked(e)) keeper = e;
|
||||
else if (copy.isNull() && !linked(e)) copy = e;
|
||||
}
|
||||
QVERIFY(!keeper.isNull() && !copy.isNull());
|
||||
const QString shared = keeper.attribute(QStringLiteral("uuid"));
|
||||
copy.setAttribute(QStringLiteral("uuid"), shared);
|
||||
const int wires = doc.elementsByTagName(QStringLiteral("conductor")).size();
|
||||
|
||||
const QString saved = resave(write(doc));
|
||||
QVERIFY(!saved.isEmpty());
|
||||
const QDomDocument out = load(saved);
|
||||
QHash<QString, QDomElement> by_uuid;
|
||||
QList<std::pair<QString, QString>> links;
|
||||
const QDomNodeList saved_folios = out.documentElement().elementsByTagName(QStringLiteral("diagram"));
|
||||
for (int i = 0; i < saved_folios.size(); ++i)
|
||||
for (const QDomElement &e : childElements(saved_folios.at(i).toElement(),
|
||||
QStringLiteral("elements"),
|
||||
QStringLiteral("element"))) {
|
||||
const QString u = e.attribute(QStringLiteral("uuid"));
|
||||
QVERIFY2(!by_uuid.contains(u), qPrintable(QStringLiteral("uuid still shared: ") + u));
|
||||
by_uuid.insert(u, e);
|
||||
for (QDomElement l = e.firstChildElement(QStringLiteral("links_uuids"))
|
||||
.firstChildElement(QStringLiteral("link_uuid"));
|
||||
!l.isNull(); l = l.nextSiblingElement(QStringLiteral("link_uuid")))
|
||||
links.append({u, l.attribute(QStringLiteral("uuid"))});
|
||||
}
|
||||
QVERIFY(by_uuid.contains(shared));
|
||||
QVERIFY(linked(by_uuid.value(shared)));
|
||||
for (const auto &link : std::as_const(links)) {
|
||||
QVERIFY2(by_uuid.contains(link.second), "a link names a symbol not in the project");
|
||||
QVERIFY2(links.contains(std::make_pair(link.second, link.first)), "a link only one side has");
|
||||
}
|
||||
QCOMPARE(out.elementsByTagName(QStringLiteral("conductor")).size(), wires);
|
||||
}
|
||||
|
||||
void savedByUuidReopensRight()
|
||||
|
||||
Reference in New Issue
Block a user