mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
[PictureLoader] Add local override storage and resolution with matcher tests (#7311)
* [PictureLoader] Add local override storage and resolution with matcher tests * [PictureLoader] Address review comments - Make deleteAllLocalOverrides static; it does not touch instance state - Drop the now-unused hasCustomArt dead code - Rename the override install methods to installPrintingOverride / installPrintingOverrideOnLoad * [Tests] Give loader matcher tests a writable HOME in CI Under GitHub's docker runner the process uid has no passwd entry, so HOME resolves to '/' and the test-mode qttest data dir cannot be created. SettingsCache's QSettings then drops every write, getPicsPath() comes back empty, and the loader searches a blank path. Point HOME at a QTemporaryDir for the duration of the run. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
e11c915a0c
commit
cca4af0ec7
5 changed files with 386 additions and 39 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDirIterator>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
|
|
@ -41,7 +41,7 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||||
&CardPictureLoader::cardLangChanged);
|
&CardPictureLoader::cardLangChanged);
|
||||||
|
|
||||||
qRegisterMetaType<ExactCard>();
|
qRegisterMetaType<ExactCard>("ExactCard");
|
||||||
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
||||||
|
|
||||||
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
||||||
|
|
@ -209,7 +209,49 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
card.emitPixmapUpdated();
|
card.emitPixmapUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
|
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
|
||||||
|
{
|
||||||
|
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||||
|
if (picsRoot.isEmpty() || !card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir baseDir(picsRoot);
|
||||||
|
if (!baseDir.cd("downloadedPics")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = card.getInfo().getCorrectedName();
|
||||||
|
|
||||||
|
QString set, collector, uuid;
|
||||||
|
auto printing = card.getPrinting();
|
||||||
|
if (printing.getSet()) {
|
||||||
|
set = printing.getSet()->getCorrectedShortName();
|
||||||
|
collector = printing.getProperty("num");
|
||||||
|
uuid = printing.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
||||||
|
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
||||||
|
|
||||||
|
if (rel.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rel += ".png";
|
||||||
|
rel = QDir::cleanPath(rel);
|
||||||
|
|
||||||
|
QString fullPath = baseDir.filePath(rel);
|
||||||
|
|
||||||
|
if (QFile::exists(fullPath)) {
|
||||||
|
QFile::remove(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
|
||||||
|
const QPixmap &pixmap,
|
||||||
|
const bool allowOverwrite)
|
||||||
{
|
{
|
||||||
if (pixmap.isNull() || !card) {
|
if (pixmap.isNull() || !card) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -269,8 +311,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
||||||
|
|
||||||
QFileInfo outInfo(baseDir.filePath(relativePath));
|
QFileInfo outInfo(baseDir.filePath(relativePath));
|
||||||
|
|
||||||
// Do not overwrite existing files
|
// Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
|
||||||
if (outInfo.exists()) {
|
// Only the explicit override paths pass allowOverwrite == true.
|
||||||
|
if (!allowOverwrite && outInfo.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,6 +334,122 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
|
||||||
|
{
|
||||||
|
// Overriding a card with itself is the reset case, not a real override: every code path below
|
||||||
|
// would re-enter itself through emitPixmapUpdated(). Reject it outright.
|
||||||
|
if (originalCard == overrideCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardInfoPtr cardPtr = overrideCard.getCardPtr();
|
||||||
|
if (!cardPtr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate so the lambda can capture it before the connection is made
|
||||||
|
auto *connectionHandle = new QMetaObject::Connection;
|
||||||
|
|
||||||
|
*connectionHandle =
|
||||||
|
connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
|
||||||
|
[originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
|
||||||
|
// All printings share the same CardInfo, so ignore updates triggered by any
|
||||||
|
// other printing (e.g., the original card re-loading from disk).
|
||||||
|
if (printing != overrideCard.getPrinting()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
|
||||||
|
// The override art has resolved — persist it and reflect it immediately.
|
||||||
|
// Retire the connection before emitting so the refresh can't re-enter.
|
||||||
|
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
||||||
|
|
||||||
|
QObject::disconnect(*connectionHandle);
|
||||||
|
delete connectionHandle;
|
||||||
|
|
||||||
|
QPixmapCache::clear();
|
||||||
|
originalCard.emitPixmapUpdated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The art could not be resolved. Keep the connection armed so a late resolution
|
||||||
|
// still lands, and surface a visible refusal instead of a silent no-op. An
|
||||||
|
// override already on disk is left untouched and simply re-displayed.
|
||||||
|
QPixmapCache::clear();
|
||||||
|
if (!hasLocalOverrides(originalCard)) {
|
||||||
|
QPixmap refusedPixmap;
|
||||||
|
getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
|
||||||
|
QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
|
||||||
|
}
|
||||||
|
originalCard.emitPixmapUpdated();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now enqueue; if the image is already loading (deduplicated in the worker),
|
||||||
|
// the signal will still fire when it completes
|
||||||
|
CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
|
||||||
|
{
|
||||||
|
// Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
|
||||||
|
if (originalCard == overrideCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
const QString key = overrideCard.getPixmapCacheKey();
|
||||||
|
|
||||||
|
if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
|
||||||
|
// Already cached — save immediately; the caller refreshes the card.
|
||||||
|
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss or previously failed load — enqueue load and wait for the signal.
|
||||||
|
installPrintingOverrideOnLoad(originalCard, overrideCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
|
||||||
|
{
|
||||||
|
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||||
|
if (picsRoot.isEmpty() || !card) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir baseDir(picsRoot);
|
||||||
|
if (!baseDir.cd("downloadedPics")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = card.getInfo().getCorrectedName();
|
||||||
|
|
||||||
|
QString set, collector, uuid;
|
||||||
|
const PrintingInfo printing = card.getPrinting();
|
||||||
|
if (printing.getSet()) {
|
||||||
|
set = printing.getSet()->getCorrectedShortName();
|
||||||
|
collector = printing.getProperty("num");
|
||||||
|
uuid = printing.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
||||||
|
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
||||||
|
|
||||||
|
if (rel.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rel += ".png";
|
||||||
|
rel = QDir::cleanPath(rel);
|
||||||
|
|
||||||
|
if (QFile::exists(baseDir.filePath(rel))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void CardPictureLoader::clearPixmapCache()
|
void CardPictureLoader::clearPixmapCache()
|
||||||
{
|
{
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
|
|
@ -338,32 +497,3 @@ void CardPictureLoader::cardLangChanged()
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
failedAt.clear();
|
failedAt.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardPictureLoader::hasCustomArt()
|
|
||||||
{
|
|
||||||
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
|
||||||
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
|
||||||
|
|
||||||
// Check if there is at least one non-directory file in the pics path, other
|
|
||||||
// than in the "downloadedPics" subdirectory.
|
|
||||||
while (it.hasNext()) {
|
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
|
|
||||||
QFileInfo dir(it.nextFileInfo());
|
|
||||||
#else
|
|
||||||
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
|
|
||||||
// the QFileInfo from a QString which requires more system calls.
|
|
||||||
QFileInfo dir(it.next());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (it.fileName() == "downloadedPics") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
|
||||||
if (subIt.hasNext()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,17 @@ public:
|
||||||
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if the user has custom card art in the picsPath directory.
|
* @brief Check if a local override image already exists for the card.
|
||||||
* @return True if any custom art exists.
|
* @param card The card to check.
|
||||||
|
* @return True if the card has at least one locally stored override image.
|
||||||
*/
|
*/
|
||||||
static bool hasCustomArt();
|
static bool hasLocalOverrides(const ExactCard &card);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Removes all locally stored override images for the card.
|
||||||
|
* @param card The card to remove the override images of.
|
||||||
|
*/
|
||||||
|
static void deleteAllLocalOverrides(const ExactCard &card);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clears the in-memory QPixmap cache for all cards.
|
* @brief Clears the in-memory QPixmap cache for all cards.
|
||||||
|
|
@ -120,7 +127,9 @@ public slots:
|
||||||
* @param image Loaded QImage.
|
* @param image Loaded QImage.
|
||||||
*/
|
*/
|
||||||
void imageLoaded(const ExactCard &card, const QImage &image);
|
void imageLoaded(const ExactCard &card, const QImage &image);
|
||||||
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
|
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
|
||||||
|
void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
|
||||||
|
void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
|
||||||
|
// downloadedPics/; check there as a fallback so local overrides round-trip.
|
||||||
|
candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
|
||||||
|
|
||||||
for (const QString &path : candidatePaths) {
|
for (const QString &path : candidatePaths) {
|
||||||
QFileInfo fileInfo(path);
|
QFileInfo fileInfo(path);
|
||||||
QDir dir = fileInfo.dir();
|
QDir dir = fileInfo.dir();
|
||||||
|
|
@ -105,7 +109,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
|
|
||||||
QStringList files = dir.entryList(QDir::Files);
|
QStringList files = dir.entryList(QDir::Files);
|
||||||
for (const QString &file : files) {
|
for (const QString &file : files) {
|
||||||
if (!file.startsWith(baseName)) {
|
QFileInfo fi(file);
|
||||||
|
if (fi.completeBaseName() != baseName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
||||||
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
||||||
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
||||||
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
|
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
|
||||||
|
add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test)
|
||||||
|
|
||||||
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
|
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
|
||||||
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
||||||
|
|
@ -38,6 +39,17 @@ add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_mo
|
||||||
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||||
add_executable(latency_tracker_test latency_tracker_test.cpp)
|
add_executable(latency_tracker_test latency_tracker_test.cpp)
|
||||||
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
|
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
|
||||||
|
add_executable(
|
||||||
|
loader_local_matching_test
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
loader_local_matching_test.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||||
|
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
|
|
@ -79,6 +91,7 @@ if(NOT GTEST_FOUND)
|
||||||
add_dependencies(lag_monitor_test gtest)
|
add_dependencies(lag_monitor_test gtest)
|
||||||
add_dependencies(latency_tracker_test gtest)
|
add_dependencies(latency_tracker_test gtest)
|
||||||
add_dependencies(metrics_registry_test gtest)
|
add_dependencies(metrics_registry_test gtest)
|
||||||
|
add_dependencies(loader_local_matching_test gtest)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
include_directories(${GTEST_INCLUDE_DIRS})
|
include_directories(${GTEST_INCLUDE_DIRS})
|
||||||
|
|
@ -123,6 +136,9 @@ target_link_libraries(
|
||||||
)
|
)
|
||||||
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
|
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
|
||||||
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
||||||
|
target_link_libraries(
|
||||||
|
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||||
|
)
|
||||||
|
|
||||||
add_subdirectory(card_zone_algorithms)
|
add_subdirectory(card_zone_algorithms)
|
||||||
add_subdirectory(carddatabase)
|
add_subdirectory(carddatabase)
|
||||||
|
|
|
||||||
187
tests/loader_local_matching_test.cpp
Normal file
187
tests/loader_local_matching_test.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#include "client/settings/cache_settings.h"
|
||||||
|
#include "interface/card_picture_loader/card_picture_loader_local.h"
|
||||||
|
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
#include <QColor>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QImageWriter>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <libcockatrice/card/lazy_properties_hash.h>
|
||||||
|
#include <libcockatrice/card/printing/exact_card.h>
|
||||||
|
#include <libcockatrice/card/set/card_set.h>
|
||||||
|
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Builds an ExactCard with the requested identity fields.
|
||||||
|
*
|
||||||
|
* Mirrors how the client constructs cards: the set short name feeds tryLoad()'s
|
||||||
|
* setName, and the "num" printing property feeds the collector number.
|
||||||
|
*/
|
||||||
|
ExactCard cardFor(const QString &name, const QString &setShortName, const QString &collectorNumber)
|
||||||
|
{
|
||||||
|
CardSetPtr set;
|
||||||
|
if (!setShortName.isEmpty()) {
|
||||||
|
set = CardSet::newInstance(new NoopCardSetPriorityController(), setShortName, setShortName);
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyPropertiesHash properties;
|
||||||
|
if (!collectorNumber.isEmpty()) {
|
||||||
|
properties.insert("num", collectorNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExactCard(CardInfo::newInstance(name), PrintingInfo(set, properties));
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalMatcherTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
QTemporaryDir tempDir; ///< Sandboxed "pics" root for every test.
|
||||||
|
CardPictureLoaderLocal *loader = nullptr; ///< Constructed per test against the sandboxed paths.
|
||||||
|
|
||||||
|
QString picsPath() const
|
||||||
|
{
|
||||||
|
return tempDir.path() + "/pics";
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
// The loader ctor snapshots the global picture paths once, so point them at the
|
||||||
|
// sandbox before constructing it.
|
||||||
|
SettingsCache::instance().paths().setPicsPath(picsPath());
|
||||||
|
SettingsCache::instance().paths().setCustomPicsPath(picsPath() + "/CUSTOM/");
|
||||||
|
|
||||||
|
loader = new CardPictureLoaderLocal(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
delete loader;
|
||||||
|
loader = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes a valid 1x1 PNG under the sandboxed pics path.
|
||||||
|
*/
|
||||||
|
void writePngUnderPics(const QString &relativePath, const QColor &color = Qt::red)
|
||||||
|
{
|
||||||
|
const QString fullPath = picsPath() + "/" + relativePath;
|
||||||
|
ASSERT_TRUE(QDir().mkpath(QFileInfo(fullPath).absolutePath()));
|
||||||
|
|
||||||
|
QImage image(1, 1, QImage::Format_RGB32);
|
||||||
|
image.fill(color);
|
||||||
|
|
||||||
|
QImageWriter writer(fullPath, "PNG");
|
||||||
|
ASSERT_TRUE(writer.write(image));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, ExactMatchBareFileWinsOverSuffixedCompanion)
|
||||||
|
{
|
||||||
|
writePngUnderPics("downloadedPics/TestCard.png");
|
||||||
|
writePngUnderPics("downloadedPics/TestCard (1).png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "", ""));
|
||||||
|
|
||||||
|
EXPECT_FALSE(image.isNull()) << "The bare TestCard.png must be picked over its suffixed companion";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, SuffixedFileWithoutExactMatchIsNotLoaded)
|
||||||
|
{
|
||||||
|
// The pre-refactor prefix match would have accepted "TestCard (1).png" for "TestCard".
|
||||||
|
writePngUnderPics("downloadedPics/TestCard (1).png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "", ""));
|
||||||
|
|
||||||
|
EXPECT_TRUE(image.isNull()) << "A suffixed file must not satisfy an exact card-name lookup";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, SetFolderLookupIgnoresSuffixedFiles)
|
||||||
|
{
|
||||||
|
writePngUnderPics("M10/TestCard (1).png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", ""));
|
||||||
|
|
||||||
|
EXPECT_TRUE(image.isNull()) << "Set-folder lookups must also require an exact name match";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, SetFolderLookupStillResolvesExactFile)
|
||||||
|
{
|
||||||
|
writePngUnderPics("M10/TestCard.png");
|
||||||
|
writePngUnderPics("M10/TestCard (1).png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", ""));
|
||||||
|
|
||||||
|
EXPECT_FALSE(image.isNull()) << "The exact file in the set folder must still resolve";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, RootDownloadedPicsFallbackResolvesSchemeFilename)
|
||||||
|
{
|
||||||
|
// Non-set-folder export schemes (Name_Set_Collector) write straight into downloadedPics/.
|
||||||
|
writePngUnderPics("downloadedPics/TestCard_M10_1.png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
|
||||||
|
|
||||||
|
EXPECT_FALSE(image.isNull()) << "downloadedPics/TestCard_M10_1.png must resolve via the root fallback";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, RootFallbackResolvesDashSeparatedVariant)
|
||||||
|
{
|
||||||
|
writePngUnderPics("downloadedPics/TestCard-M10-1.png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
|
||||||
|
|
||||||
|
EXPECT_FALSE(image.isNull()) << "The dash-separated import variant must resolve via the root fallback";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, DownloadedPicsSetSubfolderStillResolves)
|
||||||
|
{
|
||||||
|
writePngUnderPics("downloadedPics/M10/TestCard_M10_1.png");
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
|
||||||
|
|
||||||
|
EXPECT_FALSE(image.isNull()) << "The set-subfolder export scheme must keep resolving";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalMatcherTest, SetFolderCandidateTakesPrecedenceOverRootFallback)
|
||||||
|
{
|
||||||
|
writePngUnderPics("M10/TestCard_M10_1.png", Qt::red);
|
||||||
|
writePngUnderPics("downloadedPics/TestCard_M10_1.png", Qt::blue);
|
||||||
|
|
||||||
|
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
|
||||||
|
|
||||||
|
ASSERT_FALSE(image.isNull());
|
||||||
|
EXPECT_EQ(image.pixelColor(0, 0), QColor(Qt::red)) << "The set-folder candidate must be preferred";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
// Redirect SettingsCache reads/writes (app-data location) away from the real user profile.
|
||||||
|
QStandardPaths::setTestModeEnabled(true);
|
||||||
|
|
||||||
|
// Some CI containers run as a uid without a passwd entry (e.g. GitHub's docker
|
||||||
|
// runner), so HOME resolves to "/" and the test-mode qttest data dir cannot be
|
||||||
|
// created. SettingsCache's QSettings then silently drops every write, reads come
|
||||||
|
// back empty, and the paths the loader searches are "". Give the test a writable
|
||||||
|
// HOME for the duration of the run so settings behave like on a normal machine.
|
||||||
|
QTemporaryDir home;
|
||||||
|
if (home.isValid()) {
|
||||||
|
qputenv("HOME", home.path().toLocal8Bit());
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
QLoggingCategory::setFilterRules("card_picture_loader.*=false\nsettings_cache.*=false");
|
||||||
|
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue