mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-17 11:24:14 +02:00
Fix a .qetz that cannot be opened after only a folio's title changed
With a .qetz and lazy folios (both the default), changing only the
title, author, date or revision of a folio holding a picture, while that
folio was not displayed, then saving, gave a .qetz that refused to open:
'picture "images/<sha>.png" is missing'.
Such a folio is saved from its items as read, with its new header
(Diagram::deferredXmlWithHeader()). Read from a .qetz, its <image> names
its picture's file (qetz-file) instead of holding the picture, and
QetContainer::split() takes out only pictures held inline: the file was
never written, while the folio still named it. The folio now gets its
pictures written in, from the bytes the project keeps
(QETProject::containerPicture()), as a built folio writes them.
On master, a picture on folio 2 of grafcet, iso_sfc_example, 741,
tremie_vibrante or Projet_vierge is lost this way; found on three forum
projects by comparing lazy saves with saves of every folio built. Saving
as .qet was not affected.
New tst_qetzheaderpictures (grafcet.qet: picture on folio 2, saved as
.qetz; its title changed with folios built when needed; reopened with
its picture and title): fails without this change ("picture ... is
missing"). ctest 107/107.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,6 +208,13 @@ QString QetContainer::pictureFile(const QDomElement &image)
|
||||
return image.attribute(ImageFileAttribute);
|
||||
}
|
||||
|
||||
void QetContainer::inlinePicture(QDomElement image, const QByteArray &bytes)
|
||||
{
|
||||
image.removeAttribute(ImageFileAttribute);
|
||||
image.insertBefore(image.ownerDocument().createTextNode(QString::fromLatin1(bytes.toBase64())),
|
||||
image.firstChild());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::ownUuids
|
||||
@return true if every folio, symbol and wire of @p project has a uuid
|
||||
|
||||
@@ -84,6 +84,9 @@ namespace QetContainer
|
||||
QString *error = nullptr);
|
||||
/// The file join() left the picture of @p image in, or an empty string
|
||||
QString pictureFile(const QDomElement &image);
|
||||
/// @p image, which names its picture's file (pictureFile()), given
|
||||
/// the picture itself, @p bytes, as a document read whole has it
|
||||
void inlinePicture(QDomElement image, const QByteArray &bytes);
|
||||
|
||||
/// Take what QElectroTech computes out of a folio (format 4)
|
||||
void dropComputed(QDomElement diagram, bool terminal_lists);
|
||||
|
||||
@@ -881,6 +881,17 @@ QDomElement Diagram::deferredXmlWithHeader(QDomDocument &document)
|
||||
for (QDomElement child = items.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement())
|
||||
if (!header_children.contains(child.tagName()))
|
||||
folio.appendChild(document.importNode(child, true));
|
||||
//Its pictures written in, as building it writes them. A folio read
|
||||
//from a .qetz names its pictures' files instead, which the next
|
||||
//.qetz written from this document would leave out: that project
|
||||
//then refused to open, "picture ... is missing" (F118).
|
||||
for (QDomElement image : QET::findInDomElement(folio, QStringLiteral("images"), QStringLiteral("image"))) {
|
||||
const QString file = QetContainer::pictureFile(image);
|
||||
if (file.isEmpty() || !m_project) continue;
|
||||
const QByteArray bytes = m_project->containerPicture(file);
|
||||
if (!bytes.isEmpty())
|
||||
QetContainer::inlinePicture(image, bytes);
|
||||
}
|
||||
return folio;
|
||||
}
|
||||
|
||||
|
||||
@@ -943,6 +943,18 @@ if(QET_HAS_SCRIPTING)
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A .qetz folio whose title only changed keeps its pictures when saved
|
||||
# with folios built when needed (F118), on examples/grafcet.qet.
|
||||
add_executable(
|
||||
tst_qetzheaderpictures
|
||||
tst_qetzheaderpictures.cpp)
|
||||
add_test(NAME tst_qetzheaderpictures COMMAND tst_qetzheaderpictures)
|
||||
add_dependencies(tst_qetzheaderpictures qelectrotech)
|
||||
target_link_libraries(tst_qetzheaderpictures PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_qetzheaderpictures PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# qet.elementTerminals() lists each terminal's uuid and qet.terminalIndex()
|
||||
# turns element uuid + terminal uuid back into the index the conductor
|
||||
# calls take, on examples/perceuse.qet (two terminals at one point).
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// A folio of a .qetz whose title only changed is saved from its items as
|
||||
// read, not built (Diagram::deferredXmlWithHeader()). Those items name
|
||||
// their pictures' files in the .qetz, which the new .qetz left out: the
|
||||
// project saved then refused to open, 'picture "images/..." is missing'
|
||||
// (FINDINGS F118). Runs scripts through the real binary's --run on a copy
|
||||
// of examples/grafcet.qet given a picture on its second folio.
|
||||
class tst_qetzheaderpictures : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on @p project; its output
|
||||
QString run(const QString &script, const QString &project, bool every_folio_built)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("script.js"));
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) return QString();
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
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("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());
|
||||
if (every_folio_built)
|
||||
env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1"));
|
||||
else
|
||||
env.remove(QStringLiteral("QET_EAGER_FOLIOS"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), path, project});
|
||||
proc.waitForFinished(120000);
|
||||
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void titleChangeKeepsPictures()
|
||||
{
|
||||
// A 1x1 PNG
|
||||
const QString png = m_dir.filePath(QStringLiteral("dot.png"));
|
||||
QFile p(png);
|
||||
QVERIFY(p.open(QIODevice::WriteOnly));
|
||||
p.write(QByteArray::fromBase64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="));
|
||||
p.close();
|
||||
const QString with_picture = m_dir.filePath(QStringLiteral("picture.qetz"));
|
||||
const QString titled = m_dir.filePath(QStringLiteral("titled.qetz"));
|
||||
QString out = run(QStringLiteral("qet.log('ADDED ' + qet.addImage(1, '%1', 100, 100));"
|
||||
"qet.save('%2');").arg(png, with_picture),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/grafcet.qet"), true);
|
||||
QVERIFY2(out.contains(QStringLiteral("ADDED 0")), qPrintable(out));
|
||||
QVERIFY(QFile::exists(with_picture));
|
||||
|
||||
// Only the title of the folio with the picture, folios built
|
||||
// when needed
|
||||
out = run(QStringLiteral("qet.log('SET ' + qet.setFolioProperty(1, 'title', 'Edited'));"
|
||||
"qet.save('%1');").arg(titled), with_picture, false);
|
||||
QVERIFY2(out.contains(QStringLiteral("SET true")), qPrintable(out));
|
||||
QVERIFY(QFile::exists(titled));
|
||||
|
||||
// Opens, with its picture and the new title
|
||||
out = run(QStringLiteral("qet.log('PICTURES ' + qet.images(1).length);"
|
||||
"qet.log('TITLE ' + qet.folioTitle(1));"), titled, true);
|
||||
QVERIFY2(out.contains(QStringLiteral("PICTURES 1")), qPrintable(out));
|
||||
QVERIFY2(out.contains(QStringLiteral("TITLE Edited")), qPrintable(out));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_qetzheaderpictures)
|
||||
|
||||
#include "tst_qetzheaderpictures.moc"
|
||||
Reference in New Issue
Block a user