mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-19 20:44:16 +02:00
Merge pull request #1556 from ispyisail/test/round-trip-gate
Test the round trip between .qet and .qetz in CI
This commit is contained in:
@@ -388,34 +388,39 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
|
||||
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
|
||||
dropComputed(diagram, own_uuids, defined);
|
||||
|
||||
//The parts of the folios kept as they were, and their pictures
|
||||
//The parts of the folios, in the order of the folios whether they
|
||||
//were kept as they were or built: which folios were built must not
|
||||
//change the file. The names of the parts kept are taken first, so a
|
||||
//folio built cannot take one.
|
||||
QMap<QString, QByteArray> pictures; //name -> png, once each
|
||||
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
|
||||
{
|
||||
const QList<QDomElement> diagrams = childElements(root, QStringLiteral("diagram"));
|
||||
for (const QDomElement &diagram : diagrams) {
|
||||
const QString kept = diagram.attribute(QStringLiteral("qetz-kept-part"));
|
||||
if (kept.isEmpty()) continue;
|
||||
if (!kept_from.contains(kept)) {
|
||||
setError(error, QStringLiteral("the part \"%1\" of a folio kept as it was is missing").arg(kept));
|
||||
return {};
|
||||
}
|
||||
taken.insert(kept);
|
||||
const QByteArray part = kept_from.value(kept);
|
||||
static const QRegularExpression image_file(QStringLiteral("%1=\"(images/[^\"]+)\"").arg(ImageFileAttribute));
|
||||
for (const auto &match : image_file.globalMatch(QString::fromUtf8(part))) {
|
||||
const QString name = match.captured(1);
|
||||
if (!kept_from.contains(name)) {
|
||||
setError(error, QStringLiteral("the picture \"%1\" of a folio kept as it was is missing").arg(name));
|
||||
return {};
|
||||
}
|
||||
pictures.insert(name, kept_from.value(name));
|
||||
}
|
||||
parts.append(QetZip::Entry{kept, part, true});
|
||||
root.replaceChild(placeholder(document, kept), diagram);
|
||||
if (!kept.isEmpty()) taken.insert(kept);
|
||||
}
|
||||
|
||||
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
|
||||
for (QDomElement diagram : diagrams)
|
||||
{
|
||||
++folio;
|
||||
const QString kept = diagram.attribute(QStringLiteral("qetz-kept-part"));
|
||||
if (!kept.isEmpty()) {
|
||||
if (!kept_from.contains(kept)) {
|
||||
setError(error, QStringLiteral("the part \"%1\" of a folio kept as it was is missing").arg(kept));
|
||||
return {};
|
||||
}
|
||||
const QByteArray part = kept_from.value(kept);
|
||||
static const QRegularExpression image_file(QStringLiteral("%1=\"(images/[^\"]+)\"").arg(ImageFileAttribute));
|
||||
for (const auto &match : image_file.globalMatch(QString::fromUtf8(part))) {
|
||||
const QString name = match.captured(1);
|
||||
if (!kept_from.contains(name)) {
|
||||
setError(error, QStringLiteral("the picture \"%1\" of a folio kept as it was is missing").arg(name));
|
||||
return {};
|
||||
}
|
||||
pictures.insert(name, kept_from.value(name));
|
||||
}
|
||||
parts.append(QetZip::Entry{kept, part, true});
|
||||
root.replaceChild(placeholder(document, kept), diagram);
|
||||
continue;
|
||||
}
|
||||
for (QDomElement images : childElements(diagram, QStringLiteral("images"))) {
|
||||
for (QDomElement image : childElements(images, QStringLiteral("image"))) {
|
||||
const QDomText text = image.firstChild().toText();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
#include <QtTest>
|
||||
#include <QSet>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
@@ -35,7 +36,8 @@ class tst_resaveunchanged : public QObject
|
||||
|
||||
// --resave @p in to a new file, in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead); returns the new file's path.
|
||||
QString resave(const QString &in, const QString &suffix = QStringLiteral(".qet"))
|
||||
QString resave(const QString &in, const QString &suffix = QStringLiteral(".qet"),
|
||||
bool eager = false)
|
||||
{
|
||||
//Always p.qet, in its own folder: an untitled project takes its
|
||||
//file name as title, so two names would make two projects.
|
||||
@@ -48,6 +50,11 @@ class tst_resaveunchanged : public QObject
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
if (eager) {
|
||||
env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening
|
||||
} else {
|
||||
env.remove(QStringLiteral("QET_EAGER_FOLIOS"));
|
||||
}
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
|
||||
@@ -385,6 +392,117 @@ private slots:
|
||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
||||
}
|
||||
|
||||
// The round trip between the formats (#1512): every example, and
|
||||
// fixtures/every_feature.qet, a project using every feature
|
||||
// QElectroTech saves (built through its script API, the few things the
|
||||
// API cannot set added in the form QElectroTech saves them, then opened
|
||||
// and saved by QElectroTech). For each, the save stamps aside:
|
||||
// - a .qetz saved again is the same file;
|
||||
// - a .qetz saved as a .qet is the project saved straight as a .qet;
|
||||
// - that .qet saved as a .qetz is the project saved straight as a
|
||||
// .qetz, part by part, its database row by row;
|
||||
// - lazy folios save what every folio built saves, in both formats.
|
||||
void roundTripBetweenFormats_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
const QDir examples(QStringLiteral(QET_EXAMPLES_DIR));
|
||||
for (const QString &name : examples.entryList({QStringLiteral("*.qet")}, QDir::Files, QDir::Name))
|
||||
QTest::newRow(name.toUtf8().constData()) << examples.filePath(name);
|
||||
QTest::newRow("every_feature.qet") << QFINDTESTDATA("fixtures/every_feature.qet");
|
||||
}
|
||||
void roundTripBetweenFormats()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString qet = resave(project);
|
||||
QVERIFY2(!qet.isEmpty(), "--resave as .qet failed");
|
||||
const QString qetz = resave(project, QStringLiteral(".qetz"));
|
||||
QVERIFY2(!qetz.isEmpty(), "--resave as .qetz failed");
|
||||
|
||||
const QString qetz_again = resave(qetz, QStringLiteral(".qetz"));
|
||||
QVERIFY2(!qetz_again.isEmpty(), "--resave of the .qetz failed");
|
||||
QVERIFY2(read(qetz_again) == read(qetz), "a .qetz saved again changed");
|
||||
|
||||
const QString back = resave(qetz);
|
||||
QVERIFY2(!back.isEmpty(), "--resave of the .qetz as .qet failed");
|
||||
QCOMPARE(withoutSaveStamps(read(back)), withoutSaveStamps(read(qet)));
|
||||
|
||||
const QString zipped = resave(qet, QStringLiteral(".qetz"));
|
||||
QVERIFY2(!zipped.isEmpty(), "--resave of the .qet as .qetz failed");
|
||||
QCOMPARE(containerContent(zipped), containerContent(qetz));
|
||||
|
||||
const QString eager = resave(project, QStringLiteral(".qet"), true);
|
||||
QVERIFY2(!eager.isEmpty(), "--resave with every folio built failed");
|
||||
QCOMPARE(withoutSaveStamps(read(eager)), withoutSaveStamps(read(qet)));
|
||||
const QString eager_zipped = resave(project, QStringLiteral(".qetz"), true);
|
||||
QVERIFY2(!eager_zipped.isEmpty(), "--resave as .qetz with every folio built failed");
|
||||
QCOMPARE(containerContent(eager_zipped), containerContent(qetz));
|
||||
}
|
||||
|
||||
static QString withoutSaveStamps(const QByteArray &xml)
|
||||
{
|
||||
static const QRegularExpression stamp(QStringLiteral(
|
||||
"\\s*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
|
||||
return QString::fromUtf8(xml).remove(stamp);
|
||||
}
|
||||
|
||||
// What the .qetz @p path holds, its save stamps aside: one line per
|
||||
// part, an XML part as its text, project.sqlite as its rows
|
||||
static QStringList containerContent(const QString &path)
|
||||
{
|
||||
QList<QetZip::Entry> entries;
|
||||
if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")};
|
||||
QStringList content;
|
||||
for (const QetZip::Entry &entry : std::as_const(entries)) {
|
||||
if (entry.name == QLatin1String("project.sqlite")) {
|
||||
for (const QString &row : databaseRows(entry.data))
|
||||
content << entry.name + QLatin1Char(' ') + row;
|
||||
} else if (entry.name.endsWith(QLatin1String(".xml"))) {
|
||||
content << entry.name + QLatin1Char(' ') + withoutSaveStamps(entry.data);
|
||||
} else {
|
||||
content << entry.name + QLatin1Char(' ')
|
||||
+ QString::fromLatin1(QCryptographicHash::hash(entry.data, QCryptographicHash::Sha1).toHex());
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Every row of every table of the SQLite database @p database, but the
|
||||
// save stamps, as text
|
||||
static QStringList databaseRows(const QByteArray &database)
|
||||
{
|
||||
sqlite3 *db = nullptr;
|
||||
sqlite3_open(":memory:", &db);
|
||||
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
|
||||
memcpy(copy, database.constData(), size_t(database.size()));
|
||||
sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
|
||||
QStringList tables;
|
||||
sqlite3_stmt *list = nullptr;
|
||||
sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", -1, &list, nullptr);
|
||||
while (list && sqlite3_step(list) == SQLITE_ROW)
|
||||
tables << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(list, 0)));
|
||||
sqlite3_finalize(list);
|
||||
static const QRegularExpression stamp(QStringLiteral(
|
||||
"\\bsaved(filename|filepath|filenamedir|date|date-us|date-eu|time)\\b"));
|
||||
QStringList rows;
|
||||
for (const QString &table : std::as_const(tables)) {
|
||||
sqlite3_stmt *select = nullptr;
|
||||
sqlite3_prepare_v2(db, QStringLiteral("SELECT * FROM \"%1\"").arg(table).toUtf8().constData(),
|
||||
-1, &select, nullptr);
|
||||
while (select && sqlite3_step(select) == SQLITE_ROW) {
|
||||
QStringList values;
|
||||
for (int c = 0 ; c < sqlite3_column_count(select) ; ++c) {
|
||||
const unsigned char *text = sqlite3_column_text(select, c);
|
||||
values << (text ? QString::fromUtf8(reinterpret_cast<const char *>(text)) : QStringLiteral("NULL"));
|
||||
}
|
||||
const QString row = table + QLatin1Char('|') + values.join(QLatin1Char('|'));
|
||||
if (!row.contains(stamp)) rows << row;
|
||||
}
|
||||
sqlite3_finalize(select);
|
||||
}
|
||||
sqlite3_close(db);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The tables of the database in the .qetz @p path: the document's
|
||||
// engineering data only, each fact once -- not QElectroTech's own
|
||||
// project database, built from it when the project opens
|
||||
|
||||
Reference in New Issue
Block a user