From 3c40bf5bd12b2a6164f7541f92ca1e460b6c9e68 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 10:06:15 +1300 Subject: [PATCH 1/4] Tests: fix the two tests that fail on Windows Windows CI stopped running tests (bdd3b2050) after these two failed: - tst_reporttextafterinfo wrote "columns numbered from 1" to a .conf under XDG_CONFIG_HOME, which only Linux reads. On Windows the setting stayed in the registry, columns counted from 0 and the report read 1-C8. The setting now goes to an INI file QET_SETTINGS_DIR points at (#1178), read on every system. - tst_projectlock::readOnlyFolderDoesNotBlock relies on a read-only folder refusing new files. Windows ignores that flag on folders, so the lock was taken; the case is skipped there. Co-Authored-By: Claude Opus 5.5 --- tests/qttest/tst_projectlock.cpp | 4 +++- tests/qttest/tst_reporttextafterinfo.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/qttest/tst_projectlock.cpp b/tests/qttest/tst_projectlock.cpp index 8befad6e2..225d14615 100644 --- a/tests/qttest/tst_projectlock.cpp +++ b/tests/qttest/tst_projectlock.cpp @@ -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 diff --git a/tests/qttest/tst_reporttextafterinfo.cpp b/tests/qttest/tst_reporttextafterinfo.cpp index 4f8cc4acc..ecc015a9a 100644 --- a/tests/qttest/tst_reporttextafterinfo.cpp +++ b/tests/qttest/tst_reporttextafterinfo.cpp @@ -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")); From ea9da9b2c86fba9069213c79d645b8a1da2542e1 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 09:43:32 +1300 Subject: [PATCH 2/4] Fix the saved date and time lagging one save in the title block The title block variables of a save (saveddate, saveddate-eu, saveddate-us, savedtime, savedfilename, savedfilepath) were set after the file was written, and also when a project was opened. A file therefore held the previous save's date and path: after editing and saving, the title block showed the date of the save before. And every save but the first in a session wrote the time of the one before, so a project saved twice without a change gave two different files, a diff for nothing in a project kept in git (discussion #1512). stampSaved() now sets them before writing, when the project has unsaved changes (the modified flag or the undo stack moved since the last save, as the window's "[modified]" uses) or is written to another file. Otherwise they keep what the file holds, and opening a project no longer sets them. tst_savedstamps: an unchanged project saved twice in place is byte-identical; a moved symbol then a save gives today's date and the file's own path. Both fail without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR --- sources/qetproject.cpp | 73 +++++++++++----- sources/qetproject.h | 5 ++ tests/qttest/CMakeLists.txt | 12 +++ tests/qttest/tst_savedstamps.cpp | 143 +++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 tests/qttest/tst_savedstamps.cpp diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index b78411f76..eaa39afc9 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -474,6 +474,8 @@ QETProject::ProjectState QETProject::openFile(QFile *file) } QFileInfo fi(*file); setFilePath(fi.absoluteFilePath()); + //Saved again unchanged, the file keeps its saved date and path + m_stamped_path = m_file_path; //Extract the content of the xml. The raw bytes are kept : a project //file without a persisted uuid derives its uuid from them. @@ -1161,16 +1163,8 @@ void QETProject::setFilePath(const QString &filepath) setReadOnly(false); } - //title block variables should be updated after file save as dialog is confirmed, before file is saved. - m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat)); - m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy")); - m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd")); - m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm")); - m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName()); - m_project_properties.addValue("savedfilepath", filePath()); - - - + //The saved date, time and path of the title block are set by + //write(), when the project is saved (stampSaved()) emit projectFilePathChanged(this, m_file_path); emit projectInformationsChanged(this); updateDiagramsFolioData(); @@ -2801,6 +2795,33 @@ bool QETProject::changedOnDisk(QDateTime *when) const return true; } +/** + @brief QETProject::stampSaved + Set the title block variables of the save (saveddate, saveddate-eu, + saveddate-us, savedtime, savedfilename, savedfilepath) to now and to + the file being written, when there is something new to save: the + project changed since it was opened or last saved, or it is written to + another file. Otherwise they keep what the file holds, so saving an + unchanged project gives an identical file (F124). + @return true if they were set +*/ +bool QETProject::stampSaved() +{ + const bool changed = m_modified || m_undo_stack->index() != m_stamped_undo_index; + if (!changed && m_file_path == m_stamped_path) { + return false; + } + m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat)); + m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy")); + m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd")); + m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm")); + m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName()); + m_project_properties.addValue("savedfilepath", filePath()); + emit projectInformationsChanged(this); + updateDiagramsFolioData(); + return true; +} + /** @brief QETProject::write Save the project in a file. @@ -2868,13 +2889,28 @@ QETResult QETProject::write(bool overwrite_changed) if (!PreviousVersions::keep(m_file_path, PreviousVersions::count(), &backup_error)) qWarning().noquote() << "Previous version of" << m_file_path << "not kept:" << backup_error; + //The saved date, time and path go into the file being written, and + //only when there is something new to save: an unchanged project + //saves to an identical file (F124, a project kept in git). A + //failed write puts the previous values back. + const DiagramContext stamped_before = m_project_properties; + const bool stamped = stampSaved(); + const auto failed_stamped = [&](const QString &error) { + if (stamped) { + m_project_properties = stamped_before; + emit projectInformationsChanged(this); + updateDiagramsFolioData(); + } + return failed(error); + }; + QString error_message; if (isContainerPath(m_file_path)) { //A container is written from the whole document: every folio //built (stage D4 writes it from the database) if (!writeContainerFile(m_file_path, &error_message)) - return(failed(error_message)); + return(failed_stamped(error_message)); } else { @@ -2884,7 +2920,7 @@ QETResult QETProject::write(bool overwrite_changed) QDomDocument xml_project(toXml(&kept_folios)); if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios), m_file_path, &error_message)) - return(failed(error_message)); + return(failed_stamped(error_message)); } rememberDiskState(); @@ -2905,17 +2941,8 @@ QETResult QETProject::write(bool overwrite_changed) } } - //title block variables should be updated after file save dialog is confirmed, before file is saved. - m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat)); - m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd")); - m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy")); - m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm")); - m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName()); - m_project_properties.addValue("savedfilepath", filePath()); - - emit projectInformationsChanged(this); - updateDiagramsFolioData(); - + m_stamped_path = m_file_path; + m_stamped_undo_index = m_undo_stack->index(); setModified(false); return(QETResult()); } diff --git a/sources/qetproject.h b/sources/qetproject.h index 4fbba9047..80a634407 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -441,6 +441,7 @@ class QETProject : public QObject } private: + bool stampSaved(); void readProjectXml(QDomDocument &xml_project); void readDiagramsXml(QDomDocument &xml_project); void readElementsCollectionXml(QDomDocument &xml_project); @@ -570,6 +571,10 @@ class QETProject : public QObject bool m_kept_folios_trusted = false; /// Read from a .qet whose path nobody chose since: saved beside it bool m_opened_from_qet = false; + /// The file the saved date and path were set for, and the undo + /// index then: an unchanged project keeps them (stampSaved()) + QString m_stamped_path; + int m_stamped_undo_index = 0; /// The project file as this QET last read or wrote it, to see /// whether something else changed it since (changedOnDisk()) QString m_disk_path; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index a54290356..768ea7d99 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -889,6 +889,18 @@ if(QET_HAS_SCRIPTING) target_compile_definitions(tst_scriptconductoruuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"") + # 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=\"$\"") + # qet.terminalPosition() and qet.conductorPath(), checked against each # other on a fixture's conductors. add_executable( diff --git a/tests/qttest/tst_savedstamps.cpp b/tests/qttest/tst_savedstamps.cpp new file mode 100644 index 000000000..3e64c1277 --- /dev/null +++ b/tests/qttest/tst_savedstamps.cpp @@ -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 . +*/ +#include +#include +#include +#include +#include + +/** + 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 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 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 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" From 1c0deb2bc8f8f74de43c4734dfa1aa12a9b18486 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 15:01:26 +1300 Subject: [PATCH 3/4] Give each copy of a symbol sharing a uuid its own uuid when a project opens Older versions' copy and paste could leave two symbols with the same uuid. Wires, links and the project database name a symbol by its uuid, so a copy was ambiguous: #1417 worked around it for wires, but the project database keeps one row per uuid, and a link to the uuid could mean either copy. When a project opens, every symbol carrying a uuid another symbol also carries gets a uuid of its own, derived from the shared one and its folio's uuid, so the same file gives the same result on every open. The copy that keeps the shared uuid is the first one linked to another symbol (its partner's link names that uuid), else the first. It is done once the folio's wires are connected, so #1417's choice of which copy a wire meets is kept. Folios holding such a copy are built when the project opens, also with lazy folios. Only files with shared uuids change, once: 3 of the 341 corpus projects. tst_sharedsymboluuid: the lamps given one uuid now come back with three; a linked contact and an unlinked symbol sharing its uuid keep every link and wire, the linked one keeping the uuid. Both fail without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR --- sources/diagram.cpp | 17 +++++++ sources/qetproject.cpp | 59 +++++++++++++++++++++++- sources/qetproject.h | 8 +++- tests/qttest/tst_sharedsymboluuid.cpp | 66 ++++++++++++++++++++++++--- 4 files changed, 142 insertions(+), 8 deletions(-) diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 5dc44f471..9940ced53 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -2342,6 +2342,23 @@ bool Diagram::fromXml(QDomElement &document, << "wire end(s) on a terminal that moved since the project was saved," << "matched to it by place"; } + //A copy of a symbol sharing its uuid with another (older versions' + //copies, F100) gets its own, now that its wires are connected to + //it: wires and links then name one symbol each + //(QETProject::renewedSharedUuid(), only while the project opens) + if (consider_informations && m_project) { + const int folio_index = m_project->folioIndex(this); + QHash ordinal; + for (Element *element : std::as_const(added_elements)) { + const QUuid shared = element->uuid(); + const QUuid renewed = m_project->renewedSharedUuid(folio_index, ordinal[shared]++, + shared, uuid()); + if (!renewed.isNull()) { + element->setUuid(renewed); + } + } + } + if (consider_informations && !m_wires_not_reconnected.isEmpty()) { qWarning().noquote() << "Diagram::fromXml():" << m_wires_not_reconnected.size() diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index eaa39afc9..c32f1f8b8 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -299,6 +299,27 @@ QByteArray QETProject::containerPicture(const QString &file) const return m_container_pictures.value(file); } +/** + @brief QETProject::renewedSharedUuid + While the project opens: the uuid of the @p ordinal -th symbol carrying + @p uuid on the folio at @p folio_index (in the file's order), when + other symbols of the project carry the same uuid and this copy is not + the one keeping it (readDiagramsXml()). Derived from the uuid and the + folio's uuid, so the same file gives the same uuid on every open. + @return a null uuid when this symbol keeps its uuid +*/ +QUuid QETProject::renewedSharedUuid(int folio_index, int ordinal, const QUuid &uuid, + const QUuid &folio_uuid) +{ + const auto keeper = m_shared_uuid_keepers.constFind(uuid); + if (keeper == m_shared_uuid_keepers.constEnd() + || (keeper->first == folio_index && keeper->second == ordinal)) { + return QUuid(); + } + return derivedItemUuid(QStringLiteral("element-copy"), + uuid.toString() + QLatin1Char('\n') + folio_uuid.toString()); +} + /** @brief QETProject::derivedItemUuid A uuid for an item of this project that was saved without one, the same @@ -3664,6 +3685,41 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) } } + //Symbols sharing a uuid (copies made by older versions, F100): the + //project database, links and wires name a symbol by its uuid, so + //each copy but one gets its own (renewedSharedUuid()). The copy + //that keeps it is the first one linked to another symbol (a + //partner's link names the uuid), else the first. The folios with + //a copy to renew are built now, also with lazy folios, so the + //whole project has one uuid per symbol from the start. + m_shared_uuid_keepers.clear(); + QSet build_now; + { + QHash>> copies; + for (int i = 0 ; i < diagram_nodes.length() ; ++i) { + const QDomElement elements = diagram_nodes.at(i).toElement() + .firstChildElement(QStringLiteral("elements")); + QHash ordinal; + for (QDomElement e = elements.firstChildElement(QStringLiteral("element")); + !e.isNull() ; e = e.nextSiblingElement(QStringLiteral("element"))) { + const QUuid uuid(e.attribute(QStringLiteral("uuid"))); + if (uuid.isNull()) continue; + const bool linked = !e.firstChildElement(QStringLiteral("links_uuids")) + .firstChildElement(QStringLiteral("link_uuid")).isNull(); + copies[uuid].append({i, ordinal[uuid]++, linked}); + } + } + for (auto it = copies.cbegin() ; it != copies.cend() ; ++it) { + if (it.value().size() < 2) continue; + auto keeper = it.value().first(); + for (const auto © : it.value()) + if (std::get<2>(copy)) { keeper = copy; break; } + m_shared_uuid_keepers.insert(it.key(), {std::get<0>(keeper), std::get<1>(keeper)}); + for (const auto © : it.value()) + if (copy != keeper) build_now.insert(std::get<0>(copy)); + } + } + if(dlgWaiting) dlgWaiting->setProgressBarRange(0, diagram_nodes.length()*3); @@ -3717,7 +3773,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) connect(diagram, &Diagram::usedTitleBlockTemplateChanged, this, &QETProject::usedTitleBlockTemplateChanged); - m_deferring_folios = m_lazy_open; + m_deferring_folios = m_lazy_open && !build_now.contains(i); diagram->initFromXml(diagram_xml_element); m_deferring_folios = false; if (!diagram->isLoaded() && !line_starts.isEmpty()) @@ -3730,6 +3786,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) } } + m_shared_uuid_keepers.clear(); updateDiagramsFolioData(); //Initialise links between elements in this project diff --git a/sources/qetproject.h b/sources/qetproject.h index 80a634407..4b3dc7986 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -123,6 +123,8 @@ class QETProject : public QObject projectDataBase *dataBase(); QUuid uuid() const; QUuid derivedItemUuid(const QString &kind, const QString &key); + QUuid renewedSharedUuid(int folio_index, int ordinal, const QUuid &uuid, + const QUuid &folio_uuid); QByteArray containerPicture(const QString &file) const; ProjectState state() const; QString openError() const; @@ -615,7 +617,11 @@ class QETProject : public QObject int m_next_backup_slot = 0; QUuid m_uuid = QUuid::createUuid(); QHash m_derived_uuid_keys; - QSet m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid() + QSet m_saved_item_uuids; + /// While opening: per uuid carried by several symbols, the copy + /// keeping it (folio index, ordinal on that folio), see + /// renewedSharedUuid() + QHash> m_shared_uuid_keepers; //symbol and wire uuids the file carries, see derivedItemUuid() projectDataBase m_data_base; QVector m_terminal_strip_vector; QVector m_cables; diff --git a/tests/qttest/tst_sharedsymboluuid.cpp b/tests/qttest/tst_sharedsymboluuid.cpp index 85ba9bf95..f2c2671e5 100644 --- a/tests/qttest/tst_sharedsymboluuid.cpp +++ b/tests/qttest/tst_sharedsymboluuid.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include @@ -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 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 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 by_uuid; + QList> 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() From ee2a060b1604f04c94f5171b5206d3aecde0915a Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 15:47:59 +1300 Subject: [PATCH 4/4] Write a symbol's link rows in the order it saves them, with its waiting partners The project database's link table should say what each symbol saves: QETZ-DB-FIRST stage 2a moves link reading and writing to it. Measured over the corpus and 25 kinds of edit, its rows disagreed with the file for three reasons; this fixes the one in the database: - rows followed the order partners were built in, not the order the symbol saves them (11 symbols, mostly .qetz files where an edit built a partner's folio, with lazy folios); - the rows were written by three pieces of code with three rules: the rebuild and the flush from the built partners then the waiting ones, the save from its own list. Element::linksToSave() is now that one list: the order the symbol was saved with, then partners linked since, a partner on a folio not built included. toXml() writes it and the database's rows are written from it (projectDataBase::insertLinkRows()), with their place in a new ord column of the in-memory table (the .qetz file's own link table is not touched). A built partner the project no longer has, on a removed folio, still gets no row, as before. The other two reasons are not here: symbols sharing a uuid (PR #1522) and links saved to symbols on a removed folio (F113), which the save will stop writing once it writes from the database (stage 2a-2). QET_CHECK_LINK_ROWS=1 says at each save whether the rows are what the symbols save, without building any folio. tst_resaveunchanged linkRowsAfterLazyEdit moves a symbol of m_000 as a .qetz with lazy folios: with the previous order, 2 symbols differ. No saved byte changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR --- sources/dataBase/projectdatabase.cpp | 144 +++++++++++++++++---------- sources/dataBase/projectdatabase.h | 3 + sources/qetgraphicsitem/element.cpp | 68 ++++++++----- sources/qetgraphicsitem/element.h | 7 ++ sources/qetproject.cpp | 10 ++ tests/qttest/tst_resaveunchanged.cpp | 30 +++++- 6 files changed, 184 insertions(+), 78 deletions(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index b688865b9..06811c9db 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -847,13 +847,15 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } } - query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " - "VALUES (:element_uuid, :linked_uuid, :group_index)")); + query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) " + "VALUES (:element_uuid, :linked_uuid, :group_index, :ord)")); for (const DocumentElement &element : std::as_const(elements)) { + int ord = 0; for (const auto &link : element.links) { query.bindValue(QStringLiteral(":element_uuid"), element.uuid); query.bindValue(QStringLiteral(":linked_uuid"), link.first); query.bindValue(QStringLiteral(":group_index"), link.second >= 0 ? QVariant(link.second) : QVariant()); + query.bindValue(QStringLiteral(":ord"), ord++); if (!query.exec()) { qDebug() << "projectDataBase::populateFromDocument link insert error : " << query.lastError(); } @@ -1092,7 +1094,7 @@ QList projectDataBase::linkedSymbols(const QUuid &element) const { QList linked; QSqlQuery query(m_data_base); - query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY rowid")); + query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY ord, rowid")); query.bindValue(QStringLiteral(":uuid"), element.toString()); if (query.exec()) { while (query.next()) @@ -2602,12 +2604,14 @@ bool projectDataBase::createDataBase() //Create the link table: one row per element and element it is //linked to -- a coil and its contacts, a pair of folio reports -- - //from each side, with the contact group the element saved for it. + //from each side, with the contact group the element saved for it, + //and its place (ord) in the order the element saves its links. const QString link_table(QStringLiteral( "CREATE TABLE link (" "element_uuid VARCHAR(50) NOT NULL, " "linked_uuid VARCHAR(50) NOT NULL, " "group_index INTEGER, " + "ord INTEGER NOT NULL DEFAULT 0, " "PRIMARY KEY (element_uuid, linked_uuid), " "FOREIGN KEY (element_uuid) REFERENCES element (uuid))")); if (!query_.exec(link_table)) { @@ -3868,10 +3872,11 @@ QString projectDataBase::prefillReport() const void projectDataBase::populateLinkTable() { m_dirty_link_elements.clear(); + const QSet waiting = waitingSymbols(); QSqlQuery query(m_data_base); query.exec(QStringLiteral("DELETE FROM link")); - query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " - "VALUES (:element_uuid, :linked_uuid, :group_index)")); + query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) " + "VALUES (:element_uuid, :linked_uuid, :group_index, :ord)")); for (const auto &diagram : m_project->folios()) { @@ -3885,23 +3890,7 @@ void projectDataBase::populateLinkTable() } const ElementProvider ep(diagram); for (const auto &elmt : ep.find(allElementTypes())) - { - QList> rows; - for (Element *linked : elmt->linkedElements()) - rows.append({linked->uuid().toString(), elmt->groupIndexForElement(linked)}); - //Partners on a folio not built, which it will be linked to - for (const auto &waiting : elmt->waitingLinks()) - rows.append({waiting.first.toString(), waiting.second}); - for (const auto &row : std::as_const(rows)) - { - query.bindValue(QStringLiteral(":element_uuid"), elmt->uuid().toString()); - query.bindValue(QStringLiteral(":linked_uuid"), row.first); - query.bindValue(QStringLiteral(":group_index"), row.second >= 0 ? QVariant(row.second) : QVariant()); - if (!query.exec()) { - qDebug() << "projectDataBase::populateLinkTable insert error : " << query.lastError(); - } - } - } + insertLinkRows(query, elmt, waiting); } } @@ -3927,6 +3916,83 @@ void projectDataBase::queueLinks(Element *element) } } +/** + @brief projectDataBase::linkRowMismatches + A test and debugging aid (QET_CHECK_LINK_ROWS): the placed + symbols whose link rows are not the links they save, in order + (Element::linksToSave(), insertLinkRows()) + @return one entry per such symbol: its uuid +*/ +QStringList projectDataBase::linkRowMismatches() +{ + flushLinks(); + const QSet waiting = waitingSymbols(); + QStringList mismatches; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link " + "WHERE element_uuid = :uuid ORDER BY ord, rowid")); + for (auto it = m_placed_elements.cbegin() ; it != m_placed_elements.cend() ; ++it) + { + Element *element = it.value(); + if (!element) { + continue; + } + QList> expected; + for (const Element::SavedLink &link : element->linksToSave(waiting)) + if (!link.built || m_placed_elements.contains(link.uuid, link.built)) + expected.append({link.uuid.toString(), link.group_index}); + QList> rows; + query.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); + if (query.exec()) + while (query.next()) + rows.append({query.value(0).toString(), + query.value(1).isNull() ? -1 : query.value(1).toInt()}); + if (rows != expected) { + mismatches << element->uuid().toString(); + } + } + return mismatches; +} + +/** + @brief projectDataBase::waitingSymbols + @return the symbols on folios not built yet (lazy folios): partners a + symbol keeps its saved links to until they are built +*/ +QSet projectDataBase::waitingSymbols() const +{ + return m_project && m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios() + : QSet(); +} + +/** + @brief projectDataBase::insertLinkRows + Write @p element's link rows with @p insert (prepared with :element_uuid, + :linked_uuid, :group_index and :ord): its partners as it saves them + (Element::linksToSave()), in that order, so the rows and the file say + the same: @p waiting is the symbols on folios not built, as a save + takes them (waitingSymbols()). A built partner the project no longer has (on a removed + folio, linked still for the undo) gets no row. +*/ +void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element, + const QSet &waiting) +{ + int ord = 0; + for (const Element::SavedLink &link : element->linksToSave(waiting)) + { + if (link.built && !m_placed_elements.contains(link.uuid, link.built)) { + continue; + } + insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); + insert.bindValue(QStringLiteral(":linked_uuid"), link.uuid.toString()); + insert.bindValue(QStringLiteral(":group_index"), link.group_index >= 0 ? QVariant(link.group_index) : QVariant()); + insert.bindValue(QStringLiteral(":ord"), ord++); + if (!insert.exec()) { + qDebug() << "projectDataBase::insertLinkRows insert error : " << insert.lastError(); + } + } +} + /** @brief projectDataBase::flushLinks Write the link rows of every element queued by linksChanged() @@ -3939,12 +4005,13 @@ void projectDataBase::flushLinks() const auto dirty = m_dirty_link_elements; m_dirty_link_elements.clear(); + const QSet waiting = waitingSymbols(); const bool own_transaction = m_data_base.transaction(); QSqlQuery remove(m_data_base); remove.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid")); QSqlQuery insert(m_data_base); - insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " - "VALUES (:element_uuid, :linked_uuid, :group_index)")); + insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) " + "VALUES (:element_uuid, :linked_uuid, :group_index, :ord)")); for (const QPointer &element : dirty) { if (!element) { @@ -3960,32 +4027,7 @@ void projectDataBase::flushLinks() if (!m_placed_elements.contains(element->uuid(), element)) { continue; } - for (Element *linked : element->linkedElements()) - { - //A symbol on a removed folio stays linked, for the undo, - //but the project no longer has it - if (!m_placed_elements.contains(linked->uuid(), linked)) { - continue; - } - const int group = element->groupIndexForElement(linked); - insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); - insert.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString()); - insert.bindValue(QStringLiteral(":group_index"), group >= 0 ? QVariant(group) : QVariant()); - if (!insert.exec()) { - qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); - } - } - //Partners on a folio not built (lazy folios), which it will - //be linked to once that folio is - for (const auto &waiting : element->waitingLinks()) - { - insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); - insert.bindValue(QStringLiteral(":linked_uuid"), waiting.first.toString()); - insert.bindValue(QStringLiteral(":group_index"), waiting.second >= 0 ? QVariant(waiting.second) : QVariant()); - if (!insert.exec()) { - qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); - } - } + insertLinkRows(insert, element, waiting); } if (own_transaction) { m_data_base.commit(); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 6b5fcc38c..a34d52536 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -154,6 +154,7 @@ class projectDataBase : public QObject void conductorPropertiesStored(Conductor *conductor, const ConductorProperties &properties); void conductorUuidChanged(Conductor *conductor, const QUuid &old_uuid); QStringList conductorPropertiesMismatches() const; + QStringList linkRowMismatches(); //Fill the three stores from a project's document before its //folios are built, then, once @@ -253,6 +254,8 @@ class projectDataBase : public QObject void populateConductorTable(); void populateLinkTable(); void flushLinks(); + void insertLinkRows(QSqlQuery &insert, Element *element, const QSet &waiting); + QSet waitingSymbols() const; void populateDrawingItemTables(); bool populateFromDocument(const QDomDocument &document, QString *why = nullptr); bool writeDrawingItem(QObject *object); diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 91fc96b92..f2d61692f 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -1437,32 +1437,11 @@ QDomElement Element::toXml( element.appendChild(inputs); //if this element is linked to other elements, - //save the uuid of each other elements - //A partner on a folio not built yet (lazy folios), which this - //symbol will be linked to, is saved too, in the order the symbol was - //saved with: saving needs no partner built (stage D5a) + //save the uuid of each other elements (linksToSave()) QList> links; - if (keepsSavedLinks() && diagram() && diagram()->project()) { - const QSet waiting = diagram()->project()->waitingLinkTargets(); - QSet written; - for (const auto &link_info : std::as_const(tmp_uuids_link)) { - Element *linked = nullptr; - for (Element *elmt : std::as_const(connected_elements)) - if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; } - if (linked) { - written.insert(linked); - links.append({linked->uuid(), m_group_index_map.value(linked, -1)}); - } else if (waiting.contains(link_info.uuid)) { - links.append({link_info.uuid, link_info.group_index}); - } - } - for (Element *elmt : std::as_const(connected_elements)) - if (!written.contains(elmt)) - links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)}); - } else { - for (Element *elmt : std::as_const(connected_elements)) - links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)}); - } + for (const SavedLink &link : linksToSave(diagram() && diagram()->project() + ? diagram()->project()->waitingLinkTargets() : QSet())) + links.append({link.uuid, link.group_index}); if (!links.isEmpty()) { QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids")); for (const auto &link : std::as_const(links)) { @@ -1869,6 +1848,45 @@ void Element::initLink(QETProject *prj) } } +/** + @brief Element::linksToSave + The partners this symbol saves, in the order it saves them: the order + it was saved with (tmp_uuids_link), then partners linked since. A + partner on a folio not built yet (lazy folios), which this symbol will + be linked to, is in the list too: saving needs no partner built (stage + D5a). toXml() writes this list, and the project database's link rows + are written from it, so the two are the same (QETZ-DB-FIRST stage 2a). + @param waiting the symbols on folios not built: a saved partner among + them is kept (QETProject::symbolsOnUnbuiltFolios()) + @return each partner's uuid, its contact group (-1 if none), and the + partner itself when it is built (nullptr for one waiting) +*/ +QList Element::linksToSave(const QSet &waiting) const +{ + QList links; + if (keepsSavedLinks() && diagram() && diagram()->project()) { + QSet written; + for (const auto &link_info : std::as_const(tmp_uuids_link)) { + Element *linked = nullptr; + for (Element *elmt : std::as_const(connected_elements)) + if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; } + if (linked) { + written.insert(linked); + links.append({linked->uuid(), m_group_index_map.value(linked, -1), linked}); + } else if (waiting.contains(link_info.uuid)) { + links.append({link_info.uuid, link_info.group_index, nullptr}); + } + } + for (Element *elmt : std::as_const(connected_elements)) + if (!written.contains(elmt)) + links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt}); + } else { + for (Element *elmt : std::as_const(connected_elements)) + links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt}); + } + return links; +} + /** @brief Element::pendingLinks @return the uuids of the partners this symbol was saved linked to and diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index c2bfca599..bf16816ba 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -227,6 +227,13 @@ class Element : public QetGraphicsItem virtual void unlinkElement(Element *) {} virtual void initLink(QETProject *); QSet pendingLinks() const; + /// A partner as the symbol saves it (linksToSave()) + struct SavedLink { + QUuid uuid; + int group_index = -1; + Element *built = nullptr; ///< nullptr: on a folio not built yet + }; + QList linksToSave(const QSet &waiting) const; bool keepsSavedLinks() const {return !tmp_uuids_link.isEmpty();} QList> waitingLinks() const; /** diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index eaa39afc9..35961b69c 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -2630,6 +2630,16 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList // Write the elements collection. project_root.appendChild(m_elements_collection->root().cloneNode(true)); + //QET_CHECK_LINK_ROWS=1: say whether the project database's link rows + //are the links each placed symbol saves, in its order, once the + //folios are written. Builds nothing (lazy folios stay as they are): + //a test and debugging aid; it changes nothing. + if (qEnvironmentVariableIntValue("QET_CHECK_LINK_ROWS") == 1) { + const QStringList links = m_data_base.linkRowMismatches(); + qWarning().noquote() << "link rows:" << links.size() << "differ" + << links.mid(0, 5).join(QStringLiteral(", ")); + } + m_waiting_link_targets.clear(); return(xml_doc); } diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index 90ebfccd3..9b4fc112d 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -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