#include "translationstore.h" #include #include #include using namespace Qt::Literals::StringLiterals; namespace { void writeText(const QString &path, const QByteArray &data) { QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); file.write(data); } QByteArray readText(const QString &path) { QFile file(path); return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray(); } } class TranslationStoreTest : public QObject { Q_OBJECT private: QTemporaryDir m_dir; QString m_videos; QString m_cache; QString m_video; QUrl m_videoUrl; // The translation as TranslationService leaves it in the cache before publishing. QString cached(const QString &key, const QByteArray &data) { const QString path = TranslationStore::cachePath(m_cache, m_videoUrl, key, u"vi"_s); writeText(path, data); return path; } private Q_SLOTS: void init() { QVERIFY(m_dir.isValid()); QDir root(m_dir.path()); root.removeRecursively(); QVERIFY(root.mkpath(u"videos"_s)); QVERIFY(root.mkpath(u"cache"_s)); m_videos = m_dir.filePath(u"videos"_s); m_cache = m_dir.filePath(u"cache"_s); m_video = m_videos + u"/Show.S01E01.mkv"_s; writeText(m_video, "video"); m_videoUrl = QUrl::fromLocalFile(m_video); } void namesFiles() { QCOMPARE(TranslationStore::languageSuffix(u"Vietnamese"_s), u"vi"_s); QCOMPARE(TranslationStore::languageSuffix(u"vietnamese"_s), u"vi"_s); QCOMPARE(TranslationStore::languageSuffix(u"Klingon!"_s), u"klingon"_s); QCOMPARE(TranslationStore::sidecarPaths(u"/films/Show.S01E01.mkv"_s, u"vi"_s), QStringList({u"/films/Show.S01E01.vi.srt"_s, u"/films/Show.S01E01.qtpotato.vi.srt"_s})); QCOMPARE(TranslationStore::cachePath(u"/cache"_s, QUrl::fromLocalFile(u"/films/Show.S01E01.mkv"_s), u"abc"_s, u"vi"_s), u"/cache/Show.S01E01.abc.vi.srt"_s); QCOMPARE(TranslationStore::cachePath(u"/cache"_s, QUrl(u"https://example.com/live.m3u8"_s), u"abc"_s, u"vi"_s), u"/cache/subtitles.abc.vi.srt"_s); QCOMPARE(TranslationStore::videoKey(QUrl(u"https://example.com/live.m3u8"_s)), u"https://example.com/live.m3u8"_s); } void writesNextToVideo() { TranslationStore::Registry registry; const QString cache = cached(u"k1"_s, "one"); const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cache); const QString sidecar = m_videos + u"/Show.S01E01.vi.srt"_s; QCOMPARE(result.path, sidecar); QVERIFY(result.nextToVideo); QCOMPARE(readText(sidecar), "one"); QVERIFY(TranslationStore::isOwned(registry, sidecar)); QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), sidecar); QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"ja"_s), QString()); QCOMPARE(registry.find(TranslationStore::videoKey(m_videoUrl), u"vi"_s)->configKey, u"k1"_s); } // Different settings give a new translation, which replaces QtPotato's own file. void replacesOwnFileWithNewSettings() { TranslationStore::Registry registry; TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two")); QCOMPARE(result.path, m_videos + u"/Show.S01E01.vi.srt"_s); QCOMPARE(readText(result.path), "two"); QCOMPARE(registry.records.size(), 1); QCOMPARE(registry.records.constFirst().configKey, u"k2"_s); QVERIFY(!QFile::exists(m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s)); } void keepsUserSubtitle() { const QString userFile = m_videos + u"/Show.S01E01.vi.srt"_s; writeText(userFile, "mine"); TranslationStore::Registry registry; QVERIFY(!TranslationStore::isOwned(registry, userFile)); const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); QCOMPARE(result.path, m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s); QCOMPARE(readText(userFile), "mine"); QCOMPARE(readText(result.path), "one"); } // A file QtPotato wrote and the user then edited counts as the user's. void keepsEditedTranslation() { TranslationStore::Registry registry; const auto first = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); writeText(first.path, "one, fixed by hand"); QVERIFY(!TranslationStore::isOwned(registry, first.path)); const auto second = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two")); QCOMPARE(second.path, m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s); QCOMPARE(readText(first.path), "one, fixed by hand"); } // Once on ".qtpotato.vi.srt", stay there instead of leaving a second copy behind. void staysOnPreviousFile() { const QString userFile = m_videos + u"/Show.S01E01.vi.srt"_s; writeText(userFile, "mine"); TranslationStore::Registry registry; const auto first = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); QFile::remove(userFile); const auto second = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two")); QCOMPARE(second.path, first.path); QCOMPARE(readText(second.path), "two"); QVERIFY(!QFile::exists(userFile)); } void fallsBackToCacheWhenFolderIsReadOnly() { QFile::setPermissions(m_videos, QFileDevice::ReadOwner | QFileDevice::ExeOwner); auto restore = qScopeGuard([this] { QFile::setPermissions(m_videos, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner); }); if (QFileInfo(m_videos).isWritable()) { QSKIP("Permissions are not enforced here (running as root?)"); } TranslationStore::Registry registry; const QString cache = cached(u"k1"_s, "one"); const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cache); QCOMPARE(result.path, cache); QVERIFY(!result.nextToVideo); QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), cache); } void fallsBackToCacheForStreams() { const QUrl stream(u"https://example.com/live.m3u8"_s); const QString cache = TranslationStore::cachePath(m_cache, stream, u"k1"_s, u"vi"_s); writeText(cache, "one"); TranslationStore::Registry registry; const auto result = TranslationStore::publish(registry, stream, u"vi"_s, u"k1"_s, "one", cache); QCOMPARE(result.path, cache); QCOMPARE(TranslationStore::savedTranslation(registry, stream, u"vi"_s), cache); } void glossaryRewriteKeepsOwnership() { TranslationStore::Registry registry; const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); writeText(result.path, "one, new glossary"); TranslationStore::recordRewrite(registry, result.path, "one, new glossary"); QVERIFY(TranslationStore::isOwned(registry, result.path)); } void savesAndLoadsRegistry() { const QString file = m_dir.filePath(u"data/translations.json"_s); TranslationStore::Registry registry; const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); // A record whose file is gone is not worth keeping. registry.put({u"/gone.mkv"_s, u"vi"_s, u"/gone.vi.srt"_s, u"k0"_s, u"00"_s}); QVERIFY(TranslationStore::save(file, registry)); const TranslationStore::Registry loaded = TranslationStore::load(file); QCOMPARE(loaded.records.size(), 1); QVERIFY(TranslationStore::isOwned(loaded, result.path)); QCOMPARE(TranslationStore::savedTranslation(loaded, m_videoUrl, u"vi"_s), result.path); QVERIFY(TranslationStore::load(m_dir.filePath(u"missing.json"_s)).records.isEmpty()); } // The registry is no longer right about a file that disappeared. void forgetsDeletedTranslation() { TranslationStore::Registry registry; const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one")); QFile::remove(result.path); QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), QString()); } }; QTEST_GUILESS_MAIN(TranslationStoreTest) #include "translationstoretest.moc"