Cover persistent preview reuse, PNG alpha, first video frame, rotation and creation based expiry

This commit is contained in:
evgeny committed 2026-10-28 15:06:06 +01:00
1 parent 806632b67d
commit ed19adc1ba
3 files changed
+130

No files matched your search

@@ -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
@@ -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
@@ -0,0 +1,96 @@
/* Настоящие JPEG/PNG и MPEG-4/MP4: первый кадр, display matrix, прозрачность, дисковый/RAM кеш и TTL по созданию. */
#include "../src/media_preview_cache.h"
#include "debug_config.h"
#include <QtTest>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
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<QString>("format"); QTest::addColumn<QColor>("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<bool>("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"