From ea9da9b2c86fba9069213c79d645b8a1da2542e1 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 09:43:32 +1300 Subject: [PATCH] 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"