Files
qelectrotech-source-mirror/tests/qttest/tst_pdfreproducible.cpp
T
ispyisail 8361a1efce Build and work on Qt before 6.8
QPdfWriter::setDocumentId() is new in Qt 6.8, QTimeZone::UTC in 6.5 and
QHashSeed in 6.2, so the build failed on Qt 6.4 (Ubuntu 24.04). Qt before
6.8 also writes no /ModDate and no XMP metadata, and setDocumentDate()
then gave up and left the time of the export in the file. Those two are
now rewritten when present, and no document id is set where Qt writes
none.

tst_pdfreproducible also checks that the xref table still matches the
file after the dates are rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJw3u5AdKQgQVPug8CzuMQ
2026-10-03 09:30:11 +13:00

120 lines
4.1 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
/*
With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
same project in every run: the dates are the ones it names, the document
id comes from the project file, and the fonts come in a fixed order.
Without it the dates are the time of the export, as before. Exported in
separate processes, since what used to differ changed between runs.
*/
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QTemporaryDir>
#include <QtTest>
class tst_pdfreproducible : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
int m_run = 0;
QByteArray exportPdf(const QString &project, const QByteArray &epoch)
{
const int run = m_run++;
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
const QString out = m_dir.filePath(QStringLiteral("out%1.pdf").arg(run));
QDir().mkpath(home);
QDir().mkpath(tmp);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
env.insert(QStringLiteral("TMPDIR"), tmp);
env.remove(QStringLiteral("QT_HASH_SEED"));
env.remove(QStringLiteral("SOURCE_DATE_EPOCH"));
if (!epoch.isEmpty())
env.insert(QStringLiteral("SOURCE_DATE_EPOCH"), QString::fromLatin1(epoch));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--export-pdf"), project, out});
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
return {};
QFile file(out);
if (!file.open(QIODevice::ReadOnly))
return {};
return file.readAll();
}
/// Every object the xref table lists starts where the table says: the
/// dates are rewritten after Qt writes the file, which moves them.
static bool xrefMatches(const QByteArray &pdf)
{
const int sx = pdf.lastIndexOf("startxref");
if (sx == -1)
return false;
const int xref = pdf.mid(sx + 9).trimmed().split('\n').value(0).toInt();
if (!pdf.mid(xref).startsWith("xref"))
return false;
const QList<QByteArray> lines = pdf.mid(xref).split('\n');
const QList<QByteArray> header = lines.value(1).split(' ');
const int first = header.value(0).toInt();
const int count = header.value(1).toInt();
for (int i = 0; i < count; ++i) {
const QList<QByteArray> entry = lines.value(2 + i).split(' ');
if (entry.value(2) != "n")
continue;
const QByteArray obj = QByteArray::number(first + i) + " 0 obj";
if (pdf.mid(entry.value(0).toInt(), obj.size()) != obj)
return false;
}
return count > 0;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
}
void sameBytesEveryRun()
{
// A project with no cross-reference links: their order is fixed
// separately.
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
QVERIFY2(QFile::exists(project), "examples/741.qet not found");
const QByteArray first = exportPdf(project, "1700000000");
QVERIFY(!first.isEmpty());
QVERIFY(first.contains("/CreationDate (D:20231114221320Z)"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
QVERIFY(first.contains("xmp:CreateDate=\"2023-11-14T22:13:20Z\""));
#endif
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
for (int i = 0; i < 3; ++i)
QVERIFY2(exportPdf(project, "1700000000") == first,
"two exports of the same project differ");
}
void nowWithoutTheVariable()
{
const QString project =
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
const QByteArray pdf = exportPdf(project, QByteArray());
QVERIFY(!pdf.isEmpty());
const QByteArray year =
"/CreationDate (D:" + QByteArray::number(QDate::currentDate().year());
QVERIFY(pdf.contains(year));
}
};
QTEST_MAIN(tst_pdfreproducible)
#include "tst_pdfreproducible.moc"