Füge Unterstützung für Regressionstests hinzu und erweitere die Benutzeroberfläche mit neuen Schaltflächen zur Genehmigung identischer Übersetzungen und zur Wiederverwendung vorhandener Übersetzungen.

This commit is contained in:
Torsten Schulz (local)
2026-10-29 09:48:07 +01:00
parent d1c68d78f1
commit 05d62aad5f
5 changed files with 508 additions and 58 deletions

156
tests/save_tests.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "mainwindow.h"
#include <QApplication>
#include <QComboBox>
#include <QDir>
#include <QFile>
#include <QLineEdit>
#include <QMessageBox>
#include <QSignalBlocker>
#include <QTemporaryDir>
#include <QTimer>
#include <QTreeWidget>
static void require(bool condition, const char *message)
{
if (!condition) qFatal("%s", message);
}
static void writeFile(const QString &path, const QString &text)
{
QFile file(path);
require(file.open(QIODevice::WriteOnly), "Cannot create fixture");
require(file.write(text.toUtf8()) == text.toUtf8().size(), "Cannot write fixture");
}
static QString readFile(const QString &path)
{
QFile file(path);
require(file.open(QIODevice::ReadOnly), "Original file is missing");
return QString::fromUtf8(file.readAll());
}
int main(int argc, char **argv)
{
QTemporaryDir home;
require(home.isValid(), "Cannot create test directory");
qputenv("HOME", home.path().toUtf8());
QApplication application(argc, argv);
const QString project = home.path() + "/project";
const QString directory = project + "/game/tl/german";
require(QDir().mkpath(directory), "Cannot create project");
const QString common = "translate german strings:\n\n # renpy/common/00action_file.rpy:42\n old \"Yes\"\n new \"Ja\"\n";
const QString unknown = "translate german strings:\n\n old \"Other\"\n new \"Anderes\"\n";
writeFile(directory + "/common.rpy", common);
writeFile(directory + "/missing.rpy", common);
writeFile(directory + "/unknown.rpy", unknown);
const QString quoted = "translate german strings:\n\n # renpy/common/test.rpy:50\n old \"A \\\"quote\\\" [name]\"\n new \"Ein \\\"Zitat\\\" [name]\"\n";
writeFile(directory + "/quoted.rpy", quoted);
const QString dialogue = "# game/test.rpy:55\ntranslate german quoted_dialogue:\n\n # e \"A \\\"quote\\\" [name]\"\n e \"Ein \\\"Zitat\\\" [name]\"\n";
writeFile(directory + "/dialogue.rpy", dialogue);
MainWindow window;
auto *tree = window.findChild<QTreeWidget *>("treeWidget");
auto *languages = window.findChild<QComboBox *>("languageCombo");
auto *editor = window.findChild<QLineEdit *>("translationEdit");
window.findChild<QLineEdit *>("projectDir")->setText(project);
{ const QSignalBlocker blocker(languages); languages->addItem("german"); }
require(QMetaObject::invokeMethod(&window, "on_languageCombo_currentTextChanged", Qt::DirectConnection,
Q_ARG(QString, "german")), "Cannot load project");
auto find = [tree](const QString &name) {
for (int i = 0; i < tree->topLevelItemCount(); ++i) {
if (tree->topLevelItem(i)->text(0) == name) return tree->topLevelItem(i);
}
return static_cast<QTreeWidgetItem *>(nullptr);
};
auto invoke = [&window](const char *slot) {
require(QMetaObject::invokeMethod(&window, slot, Qt::DirectConnection), "Cannot invoke action");
};
auto edit = [&](QTreeWidgetItem *file, const QString &text) {
require(file && file->childCount() == 1, "Fixture was not parsed");
tree->setCurrentItem(file->child(0));
editor->setText(text);
invoke("on_setTranslationButton_clicked");
};
auto save = [&](bool success) {
QString message;
QTimer::singleShot(0, [&] {
auto *box = qobject_cast<QMessageBox *>(QApplication::activeModalWidget());
require(box, "Save result dialog is missing");
require((box->icon() != QMessageBox::Critical) == success, "Unexpected save result");
message = box->text();
box->accept();
});
invoke("on_saveButton_clicked");
return message;
};
auto *commonItem = find("common.rpy");
require(commonItem && commonItem->child(0)->text(0) == "42", "Common source line was not parsed");
edit(commonItem, "Jawohl");
save(true);
const QString saved = QString(common).replace(QStringLiteral("new \"Ja\""), QStringLiteral("new \"Jawohl\""));
require(readFile(directory + "/common.rpy") == saved, "Common translation was not saved");
const auto firstBackups = QDir(directory).entryList({"common.rpy.bak.*"}, QDir::Files);
require(firstBackups.size() == 1, "Backup is missing");
require(readFile(directory + '/' + firstBackups.first()).contains("new \"Ja\""), "Backup was modified");
edit(commonItem, "Ja, bitte");
save(true);
require(QDir(directory).entryList({"common.rpy.bak.*"}, QDir::Files).size() == 2, "Repeated save collided with a backup");
auto *missing = find("missing.rpy");
require(QFile::remove(directory + "/missing.rpy"), "Cannot simulate missing source");
edit(missing, "Wiederhergestellt");
save(true);
require(readFile(directory + "/missing.rpy").contains("new \"Wiederhergestellt\""), "Loaded source was not recovered");
for (const auto &fixture : {qMakePair(QStringLiteral("quoted.rpy"), quoted),
qMakePair(QStringLiteral("dialogue.rpy"), dialogue)}) {
edit(find(fixture.first), QStringLiteral("Ein anderes \\\"Zitat\\\" [name]"));
save(true);
QString expected = fixture.second;
expected.replace(QStringLiteral("Ein \\\"Zitat\\\" [name]"), QStringLiteral("Ein anderes \\\"Zitat\\\" [name]"));
require(readFile(directory + '/' + fixture.first) == expected, "Escaped quotes corrupted a translation");
const QList<QPair<QString, QString>> inputs = {
{QStringLiteral(R"(Er sagt "Hallo" [name].)"), QStringLiteral(R"(Er sagt \"Hallo\" [name].)")},
{QStringLiteral(R"(Er sagt \"Hallo\" [name].)"), QStringLiteral(R"(Er sagt \"Hallo\" [name].)")},
{QStringLiteral(R"(Pfad\)"), QStringLiteral(R"(Pfad\\)")},
{QStringLiteral("Er sagt \"Hallo\"\nWeiter"), QStringLiteral(R"(Er sagt \"Hallo\"\nWeiter)")},
{QStringLiteral(R"(Zwei Backslashes: \\"Zitat")"), QStringLiteral(R"(Zwei Backslashes: \\\"Zitat\")")},
};
for (const auto &input : inputs) {
edit(find(fixture.first), input.first);
save(true);
expected = fixture.second;
expected.replace(QStringLiteral("Ein \\\"Zitat\\\" [name]"), input.second);
require(readFile(directory + '/' + fixture.first) == expected, "Literal quotes or backslashes produced invalid source");
// Repeated saves must not add another level of escaping.
edit(find(fixture.first), input.second);
save(true);
require(readFile(directory + '/' + fixture.first) == expected, "Repeated escaping changed the translation");
}
}
auto *unsupported = find("unknown.rpy");
edit(unsupported, "Geändert");
require(save(false).contains("Keine unterstützten Übersetzungsblöcke"), "Missing parser failure details");
require(readFile(directory + "/unknown.rpy") == unknown, "Unsupported original was changed or removed");
require(unsupported->data(0, Qt::UserRole).toBool(), "Failed save discarded dirty state");
unsupported->setData(0, Qt::UserRole, false);
// An unavailable destination must report the backup path and filesystem error.
edit(commonItem, "Noch einmal");
require(QDir().rename(directory, directory + ".held"), "Cannot move fixture directory");
const QString backupError = save(false);
require(QDir().rename(directory + ".held", directory), "Cannot restore fixture directory");
require(backupError.contains("Backup konnte nicht erstellt werden") && backupError.contains("Grund:")
&& backupError.contains("common.rpy.bak."), "Missing backup error details");
require(readFile(directory + "/common.rpy").contains("Ja, bitte"), "Failed backup changed the original");
commonItem->setData(0, Qt::UserRole, false);
auto *absent = new QTreeWidgetItem(tree, {"absent.rpy"});
absent->setData(0, Qt::UserRole + 1, directory + "/absent.rpy");
absent->setData(0, Qt::UserRole, true);
const QString missingError = save(false);
require(missingError.contains("Originaldatei fehlt") && missingError.contains("keine geladene Kopie"), "Missing source error is unclear");
qInfo("PASS: common sources, backups, repeated saves, cached recovery, original preservation, error details");
}