From ed19adc1bac96fd74ddc5c7e025cc0f690b531d5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 15:06:06 +0100 Subject: [PATCH] Cover persistent preview reuse, PNG alpha, first video frame, rotation and creation based expiry --- tools/chatgui/tests/data/preview_video.b64 | 17 ++++ .../tests/data/preview_video_rotated.b64 | 17 ++++ .../tests/test_media_preview_cache.cpp | 96 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 tools/chatgui/tests/data/preview_video.b64 create mode 100644 tools/chatgui/tests/data/preview_video_rotated.b64 create mode 100644 tools/chatgui/tests/test_media_preview_cache.cpp diff --git a/tools/chatgui/tests/data/preview_video.b64 b/tools/chatgui/tests/data/preview_video.b64 new file mode 100644 index 00000000..b8499698 --- /dev/null +++ b/tools/chatgui/tests/data/preview_video.b64 @@ -0,0 +1,17 @@ +AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAaW1kYXQAAAGzABAHAAABthYFGFBt +gsgeEbbfxtt0BYI22/cAAMEjCg2wWQPCNtv4226AsEbbfgAAAbMAEEcAAAG2FgUYKhtgeAWxtt/G238b +bfsAAMEjBUNsDwC2Ntv422/jbb9/AAADSG1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAfQAAEA +AAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAIAAAJzdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAfQAAAA +AAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAABAAAAAIAAAAAAA +JGVkdHMAAAAcZWxzdAAAAAAAAAABAAAH0AAAAAAAAQAAAAAB621kaWEAAAAgbWRoZAAAAAAAAAAAAAAA +AAAAQAAAAIAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAA +AZZtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAA +AAEAAAFWc3RibAAAAOpzdHNkAAAAAAAAAAEAAADabXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABA +ACAASAAAAEgAAAAAAAAAARNMYXZjNjIuMTEuMTAwIG1wZWc0AAAAAAAAAAAAAAAAABj//wAAAGBlc2Rz +AAAAAAOAgIBPAAEABICAgEEgEQAAAAADDUAAAAGEBYCAgC8AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiAAN +AgQEFEMAAAGyTGF2YzYyLjExLjEwMAaAgIABAgAAABBwYXNwAAAAAQAAAAEAAAAUYnRydAAAAAAAAw1A +AAABhAAAABhzdHRzAAAAAAAAAAEAAAACAABAAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAgAAAAEAAAAc +c3RzegAAAAAAAAAAAAAAAgAAADIAAAAvAAAAFHN0Y28AAAAAAAAAAQAAACwAAABhdWR0YQAAAFltZXRh +AAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0 +YQAAAAEAAAAATGF2ZjYyLjMuMTAw diff --git a/tools/chatgui/tests/data/preview_video_rotated.b64 b/tools/chatgui/tests/data/preview_video_rotated.b64 new file mode 100644 index 00000000..bbc5a392 --- /dev/null +++ b/tools/chatgui/tests/data/preview_video_rotated.b64 @@ -0,0 +1,17 @@ +AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAaW1kYXQAAAGzABAHAAABthYFGFBt +gsgeEbbfxtt0BYI22/cAAMEjCg2wWQPCNtv4226AsEbbfgAAAbMAEEcAAAG2FgUYKhtgeAWxtt/G238b +bfsAAMEjBUNsDwC2Ntv422/jbb9/AAADSG1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAfQAAEA +AAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAIAAAJzdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAfQAAAA +AAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAQAAAAABAAAAAIAAAAAAA +JGVkdHMAAAAcZWxzdAAAAAAAAAABAAAH0AAAAAAAAQAAAAAB621kaWEAAAAgbWRoZAAAAAAAAAAAAAAA +AAAAQAAAAIAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAA +AZZtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAA +AAEAAAFWc3RibAAAAOpzdHNkAAAAAAAAAAEAAADabXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAABA +ACAASAAAAEgAAAAAAAAAARNMYXZjNjIuMTEuMTAwIG1wZWc0AAAAAAAAAAAAAAAAABj//wAAAGBlc2Rz +AAAAAAOAgIBPAAEABICAgEEgEQAAAAAAAYQAAAGEBYCAgC8AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiAAN +AgQEFEMAAAGyTGF2YzYyLjExLjEwMAaAgIABAgAAABBwYXNwAAAAAQAAAAEAAAAUYnRydAAAAAAAAAGE +AAABhAAAABhzdHRzAAAAAAAAAAEAAAACAABAAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAgAAAAEAAAAc +c3RzegAAAAAAAAAAAAAAAgAAADIAAAAvAAAAFHN0Y28AAAAAAAAAAQAAACwAAABhdWR0YQAAAFltZXRh +AAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0 +YQAAAAEAAAAATGF2ZjYyLjMuMTAw diff --git a/tools/chatgui/tests/test_media_preview_cache.cpp b/tools/chatgui/tests/test_media_preview_cache.cpp new file mode 100644 index 00000000..2a2585c8 --- /dev/null +++ b/tools/chatgui/tests/test_media_preview_cache.cpp @@ -0,0 +1,96 @@ +/* Настоящие JPEG/PNG и MPEG-4/MP4: первый кадр, display matrix, прозрачность, дисковый/RAM кеш и TTL по созданию. */ +#include "../src/media_preview_cache.h" +#include "debug_config.h" +#include +#include +#include +#include +#include +using namespace MediaPreviewCache; + +class MediaPreviewCacheTest : public QObject { + Q_OBJECT + static constexpr qint64 Now = 1700000000000LL; + static bool image(const QString& path, QColor color, const char* format = "PNG") { + QImage pixels(1200, 800, color.alpha() < 255 ? QImage::Format_ARGB32 : QImage::Format_RGB32); + pixels.fill(color); return pixels.save(path, format, 95); + } +private slots: + void initTestCase() { + debug_config_init(); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_DEBUG); + } + void persistentImages_data() { + QTest::addColumn("format"); QTest::addColumn("color"); + QTest::newRow("jpeg") << QString("JPEG") << QColor(220, 30, 40); + QTest::newRow("transparent-png") << QString("PNG") << QColor(120, 60, 30, 96); + } + void persistentImages() { + QFETCH(QString, format); QFETCH(QColor, color); QTemporaryDir temporary; QVERIFY(temporary.isValid()); + const QString source = temporary.filePath("original"); QVERIFY(image(source, color, format.toLatin1().constData())); + const QString directory = temporary.filePath("previews"); Cache first(directory); + auto generated = first.load(source, Kind::Image, Now); QVERIFY2(generated.error.isEmpty(), qPrintable(generated.error)); + QCOMPARE(generated.image.size(), QSize(600, 400)); QCOMPARE(generated.created, Now); QVERIFY(QFile::exists(generated.file)); + QCOMPARE(int(generated.origin), int(Origin::Generated)); + auto memory = first.load(source, Kind::Image, Now + 1); QCOMPARE(int(memory.origin), int(Origin::Memory)); QCOMPARE(memory.created, Now); + Cache reopened(directory); auto disk = reopened.load(source, Kind::Image, Now + 2000); + QCOMPARE(int(disk.origin), int(Origin::Disk)); QCOMPARE(disk.created, Now); QCOMPARE(disk.image.size(), QSize(600, 400)); + QCOMPARE(disk.image.pixelColor(100, 100).alpha(), color.alpha()); + QVERIFY(qAbs(disk.image.pixelColor(100, 100).red() - color.red()) < 8); + } + void creationTimeExpiresWithoutTouchingOriginal() { + QTemporaryDir temporary; const QString source = temporary.filePath("original.png"); QVERIFY(image(source, Qt::green)); + Cache cache(temporary.filePath("previews")); auto original = cache.load(source, Kind::Image, Now); QVERIFY(!original.image.isNull()); + QFile record(original.file); QVERIFY(record.open(QIODevice::ReadOnly)); + QVERIFY(record.setFileTime(QDateTime::fromMSecsSinceEpoch(Now + 20 * LifetimeMs, Qt::UTC), QFileDevice::FileModificationTime)); record.close(); + const QString unrelated = temporary.filePath("previews/notes.preview"); QFile notes(unrelated); QVERIFY(notes.open(QIODevice::WriteOnly)); + notes.write("unrelated file"); notes.close(); + auto boundary = cache.load(source, Kind::Image, Now + LifetimeMs); QCOMPARE(boundary.created, Now); + QCOMPARE(cache.prune(Now + LifetimeMs).removed, 0); QVERIFY(QFile::exists(original.file)); + auto cleanup = cache.prune(Now + LifetimeMs + 1); QCOMPARE(cleanup.removed, 1); QCOMPARE(cleanup.errors, 0); + QVERIFY(!QFile::exists(original.file)); QVERIFY(QFile::exists(source)); QVERIFY(QFile::exists(unrelated)); + auto recreated = cache.load(source, Kind::Image, Now + LifetimeMs + 2); + QCOMPARE(int(recreated.origin), int(Origin::Generated)); QCOMPARE(recreated.created, Now + LifetimeMs + 2); + } + void staleAndCorruptRecordsRegenerate() { + QTemporaryDir temporary; const QString source = temporary.filePath("image.png"); QVERIFY(image(source, Qt::red)); + const QString directory = temporary.filePath("previews"); Cache first(directory); auto red = first.load(source, Kind::Image, Now); + QFile damaged(red.file); QVERIFY(damaged.open(QIODevice::WriteOnly | QIODevice::Truncate)); damaged.write("broken"); damaged.close(); + Cache reopened(directory); auto repaired = reopened.load(source, Kind::Image, Now + 1); + QCOMPARE(int(repaired.origin), int(Origin::Generated)); QVERIFY(repaired.error.isEmpty()); QVERIFY(!repaired.image.isNull()); + QVERIFY(image(source, Qt::blue)); QFile changed(source); QVERIFY(changed.open(QIODevice::ReadOnly)); + QVERIFY(changed.setFileTime(QDateTime::fromMSecsSinceEpoch(Now + 4000, Qt::UTC), QFileDevice::FileModificationTime)); changed.close(); + auto blue = reopened.load(source, Kind::Image, Now + 2); QVERIFY(blue.file != red.file); + QCOMPARE(int(blue.origin), int(Origin::Generated)); QVERIFY(blue.image.pixelColor(30, 30).blue() > 200); + } + void writeFailureRetainsUsablePreview() { + QTemporaryDir temporary; const QString source = temporary.filePath("image.png"); QVERIFY(image(source, Qt::red)); + QFile blocker(temporary.filePath("blocked")); QVERIFY(blocker.open(QIODevice::WriteOnly)); blocker.write("file"); blocker.close(); + Cache cache(blocker.fileName() + "/previews"); auto result = cache.load(source, Kind::Image, Now); + QVERIFY(!result.image.isNull()); QVERIFY(result.error.isEmpty()); QVERIFY(!QFile::exists(result.file)); + QCOMPARE(cache.prune(Now).errors, 1); + auto missing = cache.load(temporary.filePath("missing"), Kind::Image, Now); QVERIFY(missing.image.isNull()); QVERIFY(!missing.error.isEmpty()); + } + void firstVideoFrame_data() { + QTest::addColumn("rotated"); QTest::newRow("normal") << false; QTest::newRow("rotated") << true; + } + void firstVideoFrame() { +#if TEST_PREVIEW_FFMPEG + QFETCH(bool, rotated); + // Два кадра по секунде: красно-зелёный, затем синий. Второй fixture имеет display rotation=90. + const QString fixture = rotated ? QFINDTESTDATA("data/preview_video_rotated.b64") : QFINDTESTDATA("data/preview_video.b64"); + QFile data(fixture); QVERIFY(data.open(QIODevice::ReadOnly)); const QByteArray mp4 = QByteArray::fromBase64(data.readAll()); + QTemporaryDir temporary; QFile video(temporary.filePath("video.mp4")); QVERIFY(video.open(QIODevice::WriteOnly)); + QCOMPARE(video.write(mp4), mp4.size()); video.close(); Cache cache(temporary.filePath("previews")); + auto result = cache.load(video.fileName(), Kind::Video, Now); QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + QCOMPARE(result.image.size(), rotated ? QSize(32, 64) : QSize(64, 32)); + QColor first = result.image.pixelColor(8, 8); + if (rotated) QVERIFY(first.green() > 80 && first.red() < 40 && first.blue() < 40); + else QVERIFY(first.red() > 180 && first.green() < 40 && first.blue() < 40); + Cache reopened(temporary.filePath("previews")); QCOMPARE(int(reopened.load(video.fileName(), Kind::Video, Now + 1).origin), int(Origin::Disk)); +#else + QSKIP("FFmpeg is unavailable"); +#endif + } +}; +QTEST_GUILESS_MAIN(MediaPreviewCacheTest) +#include "test_media_preview_cache.moc"