#include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include 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("treeWidget"); auto *languages = window.findChild("languageCombo"); auto *editor = window.findChild("translationEdit"); window.findChild("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(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(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> 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"); }