mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'master' into tooomm_ninja-win
This commit is contained in:
commit
0467031c6e
810 changed files with 52151 additions and 9604 deletions
|
|
@ -7,21 +7,50 @@ add_test(NAME expression_test COMMAND expression_test)
|
|||
add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test)
|
||||
add_test(NAME test_age_formatting COMMAND test_age_formatting)
|
||||
add_test(NAME password_hash_test COMMAND password_hash_test)
|
||||
add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test)
|
||||
add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
|
||||
add_test(NAME server_counter_test COMMAND server_counter_test)
|
||||
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
|
||||
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
|
||||
add_test(NAME server_game_join_test COMMAND server_game_join_test)
|
||||
add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
||||
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
||||
add_test(NAME latency_tracker_test COMMAND latency_tracker_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)
|
||||
set_tests_properties(dummy_test PROPERTIES TIMEOUT 5)
|
||||
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5)
|
||||
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
||||
|
||||
add_executable(dummy_test dummy_test.cpp)
|
||||
add_executable(expression_test expression_test.cpp)
|
||||
add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp)
|
||||
add_executable(test_age_formatting test_age_formatting.cpp)
|
||||
add_executable(password_hash_test password_hash_test.cpp)
|
||||
add_executable(playmat_resolver_test playmat_resolver_test.cpp)
|
||||
add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
|
||||
add_executable(server_card_counter_test server_card_counter_test.cpp)
|
||||
add_executable(server_counter_test server_counter_test.cpp)
|
||||
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
|
||||
add_executable(server_developer_role_test server_developer_role_test.cpp)
|
||||
add_executable(server_game_join_test server_game_join_test.cpp)
|
||||
add_executable(warning_categories_test warning_categories_test.cpp)
|
||||
add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp)
|
||||
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
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(
|
||||
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 GTest
|
||||
find_package(GTest)
|
||||
|
|
@ -54,9 +83,18 @@ if(NOT GTEST_FOUND)
|
|||
add_dependencies(clamped_arithmetic_test gtest)
|
||||
add_dependencies(test_age_formatting gtest)
|
||||
add_dependencies(password_hash_test gtest)
|
||||
add_dependencies(playmat_resolver_test gtest)
|
||||
add_dependencies(deck_hash_performance_test gtest)
|
||||
add_dependencies(server_card_counter_test gtest)
|
||||
add_dependencies(server_counter_test gtest)
|
||||
add_dependencies(server_rate_limiter_test gtest)
|
||||
add_dependencies(server_developer_role_test gtest)
|
||||
add_dependencies(server_game_join_test gtest)
|
||||
add_dependencies(warning_categories_test gtest)
|
||||
add_dependencies(lag_monitor_test gtest)
|
||||
add_dependencies(latency_tracker_test gtest)
|
||||
add_dependencies(metrics_registry_test gtest)
|
||||
add_dependencies(loader_local_matching_test gtest)
|
||||
endif()
|
||||
|
||||
include_directories(${GTEST_INCLUDE_DIRS})
|
||||
|
|
@ -71,6 +109,10 @@ target_link_libraries(
|
|||
target_link_libraries(
|
||||
password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
|
|
@ -81,9 +123,35 @@ target_link_libraries(
|
|||
target_link_libraries(
|
||||
server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(
|
||||
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
target_link_libraries(
|
||||
latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
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(
|
||||
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_subdirectory(card_zone_algorithms)
|
||||
add_subdirectory(carddatabase)
|
||||
add_subdirectory(deck_list_model)
|
||||
add_subdirectory(deck_list_zones)
|
||||
add_subdirectory(loading_from_clipboard)
|
||||
add_subdirectory(movecard_tests)
|
||||
add_subdirectory(oracle)
|
||||
add_subdirectory(settings)
|
||||
|
|
|
|||
|
|
@ -134,6 +134,35 @@ TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder)
|
|||
EXPECT_EQ(knownList.at(2), &b);
|
||||
}
|
||||
|
||||
// Reconnecting to a game rebuilds zones from a ServerInfo_Zone. Non-coordinate zones
|
||||
// (hand, piles, stack) report x == 0 on every card, so inserting each rebuilt card at
|
||||
// that index would reverse the received server order. Appending (-1) keeps it.
|
||||
TEST_F(AddCardAlgorithmTest, RebuildInsertAtZeroReversesServerOrder)
|
||||
{
|
||||
MockCard a, b, c;
|
||||
CardZoneAlgorithms::addCardToList(knownList, &a, 0, false);
|
||||
CardZoneAlgorithms::addCardToList(knownList, &b, 0, false);
|
||||
CardZoneAlgorithms::addCardToList(knownList, &c, 0, false);
|
||||
|
||||
EXPECT_EQ(knownList.size(), 3);
|
||||
EXPECT_EQ(knownList.at(0), &c);
|
||||
EXPECT_EQ(knownList.at(1), &b);
|
||||
EXPECT_EQ(knownList.at(2), &a);
|
||||
}
|
||||
|
||||
TEST_F(AddCardAlgorithmTest, RebuildAppendPreservesServerOrder)
|
||||
{
|
||||
MockCard a, b, c;
|
||||
CardZoneAlgorithms::addCardToList(knownList, &a, -1, false);
|
||||
CardZoneAlgorithms::addCardToList(knownList, &b, -1, false);
|
||||
CardZoneAlgorithms::addCardToList(knownList, &c, -1, false);
|
||||
|
||||
EXPECT_EQ(knownList.size(), 3);
|
||||
EXPECT_EQ(knownList.at(0), &a);
|
||||
EXPECT_EQ(knownList.at(1), &b);
|
||||
EXPECT_EQ(knownList.at(2), &c);
|
||||
}
|
||||
|
||||
TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough)
|
||||
{
|
||||
MockCard card;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,24 @@ target_link_libraries(
|
|||
|
||||
add_test(NAME carddatabase_test COMMAND carddatabase_test)
|
||||
|
||||
# ------------------------
|
||||
# Load Benchmark (manual, not run in CI)
|
||||
# ------------------------
|
||||
add_executable(load_benchmark ${MOCKS_SOURCES} ${VERSION_STRING_CPP} load_benchmark.cpp mocks.cpp)
|
||||
|
||||
target_link_libraries(
|
||||
load_benchmark
|
||||
PRIVATE libcockatrice_card
|
||||
PRIVATE libcockatrice_models
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(load_benchmark gtest)
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
# Filter String Test
|
||||
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_data.h>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/card/lazy_properties_hash.h>
|
||||
#include <libcockatrice/card/printing/printing_info.h>
|
||||
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
|
|
@ -33,6 +38,49 @@ TEST(CardDatabaseTest, LoadXml)
|
|||
ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear";
|
||||
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
|
||||
}
|
||||
|
||||
TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
|
||||
{
|
||||
NoopCardSetPriorityController controller;
|
||||
CardSetPtr set =
|
||||
CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary);
|
||||
|
||||
QHash<QString, QString> props;
|
||||
props["manacost"] = "1R";
|
||||
PrintingInfo printing(set, LazyPropertiesHash(props));
|
||||
SetToPrintingsMap setsInfo;
|
||||
setsInfo["TST"].append(printing);
|
||||
|
||||
CardInfo::UiAttributes attributes = {.tableRow = 1};
|
||||
CardInfoPtr card =
|
||||
CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes);
|
||||
card->setLocalizedName("de", "Blitzschlag");
|
||||
card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
|
||||
SetNameMap sets;
|
||||
sets.insert("TST", set);
|
||||
CardNameMap cards;
|
||||
cards.insert("Lightning Bolt", card);
|
||||
|
||||
QTemporaryDir tempDir;
|
||||
const QString fileName = tempDir.filePath("cards.xml");
|
||||
NoopCardPreferenceProvider prefProvider;
|
||||
CockatriceXml4Parser writer(&prefProvider, &controller);
|
||||
ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName));
|
||||
|
||||
CardDatabaseData data;
|
||||
CockatriceXml4Parser parser(&prefProvider, &controller);
|
||||
QFile file(fileName);
|
||||
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
parser.parseFileInto(file, data);
|
||||
|
||||
CardInfoPtr loaded = data.cards.value("Lightning Bolt");
|
||||
ASSERT_FALSE(loaded.isNull());
|
||||
ASSERT_EQ(loaded->getName(), "Lightning Bolt");
|
||||
ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag");
|
||||
ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage.");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
235
tests/carddatabase/load_benchmark.cpp
Normal file
235
tests/carddatabase/load_benchmark.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Standalone benchmark for card database loading.
|
||||
*
|
||||
* Measures wall-clock cost and model churn:
|
||||
* - Cold load (XML -> write cache)
|
||||
* - Warm load (read cache)
|
||||
*
|
||||
* Run:
|
||||
* load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]
|
||||
*
|
||||
* All arguments are optional; they default to the XDG Cockatrice data location
|
||||
* (~/.local/share/Cockatrice/Cockatrice/...).
|
||||
*/
|
||||
|
||||
#include "mocks.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QAbstractItemModel>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QLoggingCategory>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_loader.h>
|
||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
#include <libcockatrice/models/database/card_database_model.h>
|
||||
|
||||
static QString defaultXdgPath(const QString &file)
|
||||
{
|
||||
QString base = QDir::homePath() + "/.local/share/Cockatrice/Cockatrice/";
|
||||
return base + file;
|
||||
}
|
||||
|
||||
class CliCardDatabasePathProvider : public ICardDatabasePathProvider
|
||||
{
|
||||
public:
|
||||
QString cardDb;
|
||||
QString tokens;
|
||||
QString spoilers;
|
||||
QString custom;
|
||||
|
||||
QString getCardDatabasePath() const override
|
||||
{
|
||||
return cardDb;
|
||||
}
|
||||
QString getTokenDatabasePath() const override
|
||||
{
|
||||
return tokens;
|
||||
}
|
||||
QString getSpoilerCardDatabasePath() const override
|
||||
{
|
||||
return spoilers;
|
||||
}
|
||||
QString getCustomCardDatabasePath() const override
|
||||
{
|
||||
return custom;
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
struct ChurnCounters
|
||||
{
|
||||
qint64 cardAddedEmissions = 0;
|
||||
qint64 cardRemovedEmissions = 0;
|
||||
qint64 modelRowsInserted = 0;
|
||||
qint64 modelRowsRemoved = 0;
|
||||
qint64 loadingFinishedSignals = 0;
|
||||
qint64 databaseResetSignals = 0;
|
||||
qint64 modelResets = 0;
|
||||
};
|
||||
|
||||
struct RunResult
|
||||
{
|
||||
qint64 elapsedMs = 0;
|
||||
qint64 cardCount = 0;
|
||||
qint64 setCount = 0;
|
||||
LoadStatus status = NotLoaded;
|
||||
ChurnCounters churn;
|
||||
qint64 propsQueried = 0;
|
||||
qint64 propTotal = 0;
|
||||
};
|
||||
|
||||
void deleteCache(const QString &cardDbPath)
|
||||
{
|
||||
const QString cachePath = cardDbPath + ".cache";
|
||||
if (QFile::exists(cachePath)) {
|
||||
QFile::remove(cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
RunResult runLoad(CardDatabase *db, ChurnCounters &counters)
|
||||
{
|
||||
const ChurnCounters before = counters;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
db->loadCardDatabases();
|
||||
|
||||
RunResult result;
|
||||
result.elapsedMs = timer.elapsed();
|
||||
result.cardCount = db->getCardList().size();
|
||||
result.setCount = db->getSetList().size();
|
||||
result.status = db->getLoadStatus();
|
||||
|
||||
result.churn.cardAddedEmissions = counters.cardAddedEmissions - before.cardAddedEmissions;
|
||||
result.churn.cardRemovedEmissions = counters.cardRemovedEmissions - before.cardRemovedEmissions;
|
||||
result.churn.modelRowsInserted = counters.modelRowsInserted - before.modelRowsInserted;
|
||||
result.churn.modelRowsRemoved = counters.modelRowsRemoved - before.modelRowsRemoved;
|
||||
result.churn.loadingFinishedSignals = counters.loadingFinishedSignals - before.loadingFinishedSignals;
|
||||
result.churn.databaseResetSignals = counters.databaseResetSignals - before.databaseResetSignals;
|
||||
result.churn.modelResets = counters.modelResets - before.modelResets;
|
||||
|
||||
for (const CardInfoPtr &card : db->getCardList()) {
|
||||
result.propTotal += card->getPropertiesHash().size();
|
||||
if (!card->getProperty("colors").isEmpty()) {
|
||||
++result.propsQueried;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void printResult(const QString &label, const RunResult &r)
|
||||
{
|
||||
qInfo() << "----------------------------------------";
|
||||
qInfo() << label;
|
||||
qInfo() << " Load wall-clock time :" << r.elapsedMs << "ms";
|
||||
qInfo() << " Cards loaded :" << r.cardCount;
|
||||
qInfo() << " Sets loaded :" << r.setCount;
|
||||
qInfo() << " Load status :" << r.status;
|
||||
qInfo() << " Properties queried :" << r.propsQueried << "/" << r.cardCount
|
||||
<< " cards, total props:" << r.propTotal;
|
||||
qInfo() << " CHURN during load:";
|
||||
qInfo() << " cardAdded emissions :" << r.churn.cardAddedEmissions;
|
||||
qInfo() << " cardRemoved emissions :" << r.churn.cardRemovedEmissions;
|
||||
qInfo() << " model rowsInserted :" << r.churn.modelRowsInserted;
|
||||
qInfo() << " model rowsRemoved :" << r.churn.modelRowsRemoved;
|
||||
qInfo() << " loadingFinished signals :" << r.churn.loadingFinishedSignals;
|
||||
qInfo() << " databaseReset signals :" << r.churn.databaseResetSignals;
|
||||
qInfo() << " model resets (batch) :" << r.churn.modelResets;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
QStringList args = app.arguments();
|
||||
CliCardDatabasePathProvider pathProvider;
|
||||
pathProvider.cardDb = defaultXdgPath("cards.xml");
|
||||
pathProvider.tokens = defaultXdgPath("tokens.xml");
|
||||
pathProvider.spoilers = defaultXdgPath("spoiler.xml");
|
||||
pathProvider.custom = defaultXdgPath("customsets/");
|
||||
|
||||
for (int i = 1; i < args.size(); ++i) {
|
||||
const QString &a = args.at(i);
|
||||
if (a == "--carddb" && i + 1 < args.size()) {
|
||||
pathProvider.cardDb = args.at(++i);
|
||||
} else if (a == "--tokens" && i + 1 < args.size()) {
|
||||
pathProvider.tokens = args.at(++i);
|
||||
} else if (a == "--spoilers" && i + 1 < args.size()) {
|
||||
pathProvider.spoilers = args.at(++i);
|
||||
} else if (a == "--custom" && i + 1 < args.size()) {
|
||||
pathProvider.custom = args.at(++i);
|
||||
} else if (a == "--help" || a == "-h") {
|
||||
qInfo() << "Usage: load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
QLoggingCategory::setFilterRules("card_database.loading=true\n"
|
||||
"card_database.loading.success=true\n");
|
||||
|
||||
qInfo() << "=== Card Database Load Benchmark ===";
|
||||
qInfo() << "carddb :" << pathProvider.cardDb;
|
||||
qInfo() << "tokens :" << pathProvider.tokens;
|
||||
qInfo() << "spoilers :" << pathProvider.spoilers;
|
||||
qInfo() << "custom :" << pathProvider.custom;
|
||||
qInfo() << "";
|
||||
|
||||
ChurnCounters counters;
|
||||
|
||||
auto runScenario = [&](bool deleteCacheBefore) -> RunResult {
|
||||
if (deleteCacheBefore) {
|
||||
deleteCache(pathProvider.cardDb);
|
||||
}
|
||||
|
||||
// Fresh database each scenario so signals don't accumulate across scenarios
|
||||
// for the churn counters, but we keep the same counters object to get a
|
||||
// running total if desired.
|
||||
CardDatabase db(nullptr, new NoopCardPreferenceProvider(), &pathProvider, new NoopCardSetPriorityController());
|
||||
|
||||
// Re-wire churn counters for this db instance
|
||||
QObject::connect(&db, &CardDatabase::cardAdded, &app,
|
||||
[&counters](const CardInfoPtr &) { ++counters.cardAddedEmissions; });
|
||||
QObject::connect(&db, &CardDatabase::cardRemoved, &app,
|
||||
[&counters](CardInfoPtr) { ++counters.cardRemovedEmissions; });
|
||||
QObject::connect(&db, &CardDatabase::cardDatabaseLoadingFinished, &app,
|
||||
[&counters]() { ++counters.loadingFinishedSignals; });
|
||||
QObject::connect(&db, &CardDatabase::cardDatabaseReset, &app,
|
||||
[&counters]() { ++counters.databaseResetSignals; });
|
||||
|
||||
CardDatabaseModel model(&db, false);
|
||||
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &app,
|
||||
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsInserted; });
|
||||
QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &app,
|
||||
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsRemoved; });
|
||||
QObject::connect(&model, &QAbstractItemModel::modelReset, &app, [&counters]() { ++counters.modelResets; });
|
||||
|
||||
return runLoad(&db, counters);
|
||||
};
|
||||
|
||||
// ---- Scenario 1: Cold load (XML -> write cache) ----
|
||||
printResult("Scenario 1: Cold load (XML -> cache)", runScenario(true));
|
||||
|
||||
// ---- Scenario 2: Warm load (read cache) ----
|
||||
printResult("Scenario 2: Warm load (read cache)", runScenario(false));
|
||||
|
||||
// Teardown
|
||||
deleteCache(pathProvider.cardDb);
|
||||
|
||||
qInfo() << "----------------------------------------";
|
||||
qInfo() << "Done.";
|
||||
|
||||
return 0;
|
||||
}
|
||||
33
tests/deck_list_model/CMakeLists.txt
Normal file
33
tests/deck_list_model/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
add_executable(deck_list_model_custom_zones_test ${VERSION_STRING_CPP} deck_list_model_custom_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_model_custom_zones_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
deck_list_model_custom_zones_test
|
||||
libcockatrice_models
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_model_custom_zones_test COMMAND deck_list_model_custom_zones_test)
|
||||
|
||||
add_executable(deck_list_model_zone_integration_test ${VERSION_STRING_CPP} deck_list_model_zone_integration_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_model_zone_integration_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
deck_list_model_zone_integration_test
|
||||
libcockatrice_models
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_model_zone_integration_test COMMAND deck_list_model_zone_integration_test)
|
||||
276
tests/deck_list_model/deck_list_model_custom_zones_test.cpp
Normal file
276
tests/deck_list_model/deck_list_model_custom_zones_test.cpp
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/**
|
||||
* @file deck_list_model_custom_zones_test.cpp
|
||||
* @brief Tests for the deck list model's custom-zone shadow-tree helpers.
|
||||
*
|
||||
* DeckListModelCustomZones centralizes every "what is / where is a custom zone"
|
||||
* decision for the model's shadow tree: type testing, mirroring from the deck
|
||||
* tree, name lookup, and the sort-with-custom-zones-last ordering. These tests
|
||||
* exercise that logic directly on hand-built shadow trees, independent of the
|
||||
* full model and card database machinery.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
DecklistModelCardNode *cardNode(InnerDecklistNode *parent, const QString &name, int number)
|
||||
{
|
||||
// The underlying data node is detached; only the model wrapper is attached to the shadow tree.
|
||||
auto *data = new DecklistCardNode(name, number, nullptr);
|
||||
return new DecklistModelCardNode(data, parent);
|
||||
}
|
||||
|
||||
QStringList childNames(const InnerDecklistNode *node)
|
||||
{
|
||||
QStringList names;
|
||||
for (int i = 0; i < node->size(); ++i) {
|
||||
names.append(node->at(i)->getName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================================================
|
||||
// isCustomZone
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, IsCustomZoneDistinguishesZoneFromGroup)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
auto *group = new InnerDecklistNode("Creature", board);
|
||||
auto *zone = new DecklistModelSubZoneNode("Removal", board);
|
||||
|
||||
auto *card = cardNode(group, "A", 1);
|
||||
|
||||
EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(board));
|
||||
EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(group));
|
||||
EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(card));
|
||||
EXPECT_TRUE(DeckListModelCustomZones::isCustomZone(zone));
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// findSubZoneByName
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, FindSubZoneByNameFindsAcrossBoards)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *main = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
auto *side = new InnerDecklistNode(DECK_ZONE_SIDE, &root);
|
||||
new DecklistModelSubZoneNode("Removal", main);
|
||||
new DecklistModelSubZoneNode("Utility", side);
|
||||
new InnerDecklistNode("Plain", main); // not a custom zone
|
||||
|
||||
auto *removal = DeckListModelCustomZones::findSubZoneByName(&root, "Removal");
|
||||
ASSERT_NE(removal, nullptr);
|
||||
EXPECT_EQ(removal->getName(), QString("Removal"));
|
||||
|
||||
auto *utility = DeckListModelCustomZones::findSubZoneByName(&root, "Utility");
|
||||
ASSERT_NE(utility, nullptr);
|
||||
EXPECT_EQ(utility->getName(), QString("Utility"));
|
||||
|
||||
// Names are deck-unique; a plain group or built-in board is not matched.
|
||||
EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Plain"), nullptr);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, DECK_ZONE_MAIN), nullptr);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Missing"), nullptr);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// mirrorCustomZones
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, MirrorCustomZonesCopiesCardsFlat)
|
||||
{
|
||||
// Deck-tree board zone: one direct card plus one nested custom zone.
|
||||
auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN);
|
||||
new DecklistCardNode("Direct", 2, deckBoard);
|
||||
|
||||
auto *deckZone = new InnerDecklistNode("Removal", deckBoard);
|
||||
auto *deckCard1 = new DecklistCardNode("Bolt", 3, deckZone);
|
||||
auto *deckCard2 = new DecklistCardNode("Swords", 1, deckZone);
|
||||
|
||||
InnerDecklistNode shadowRoot;
|
||||
auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot);
|
||||
|
||||
DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard);
|
||||
|
||||
// Only the custom zone is mirrored as a sub-zone; the direct card is not.
|
||||
ASSERT_EQ(shadowBoard->size(), 1);
|
||||
auto *shadowZone = dynamic_cast<DecklistModelSubZoneNode *>(shadowBoard->at(0));
|
||||
ASSERT_NE(shadowZone, nullptr);
|
||||
EXPECT_EQ(shadowZone->getName(), QString("Removal"));
|
||||
|
||||
// Cards live flat (un-grouped) inside the mirrored zone, wrapping the same data nodes.
|
||||
ASSERT_EQ(shadowZone->size(), 2);
|
||||
auto *shadowCard1 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(0));
|
||||
auto *shadowCard2 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(1));
|
||||
ASSERT_NE(shadowCard1, nullptr);
|
||||
ASSERT_NE(shadowCard2, nullptr);
|
||||
EXPECT_EQ(shadowCard1->getDataNode(), deckCard1);
|
||||
EXPECT_EQ(shadowCard2->getDataNode(), deckCard2);
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, MirrorCustomZonesWithNoCustomZonesIsNoop)
|
||||
{
|
||||
// A board zone with only direct cards has nothing to mirror.
|
||||
auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN);
|
||||
new DecklistCardNode("Direct", 2, deckBoard);
|
||||
|
||||
InnerDecklistNode shadowRoot;
|
||||
auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot);
|
||||
|
||||
DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard);
|
||||
EXPECT_EQ(shadowBoard->size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, MirrorCustomZonesFlattensNestedSubzones)
|
||||
{
|
||||
// Cards deeper than one level under a custom zone still get a model row.
|
||||
auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN);
|
||||
auto *deckZone = new InnerDecklistNode("Removal", deckBoard);
|
||||
auto *deckCard1 = new DecklistCardNode("Bolt", 1, deckZone);
|
||||
auto *deeper = new InnerDecklistNode("Deeper", deckZone);
|
||||
auto *deckCard2 = new DecklistCardNode("Swords", 1, deeper);
|
||||
|
||||
InnerDecklistNode shadowRoot;
|
||||
auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot);
|
||||
|
||||
DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard);
|
||||
|
||||
ASSERT_EQ(shadowBoard->size(), 1);
|
||||
auto *shadowZone = dynamic_cast<DecklistModelSubZoneNode *>(shadowBoard->at(0));
|
||||
ASSERT_NE(shadowZone, nullptr);
|
||||
EXPECT_EQ(shadowZone->getName(), QString("Removal"));
|
||||
|
||||
// Both cards are flattened into the mirrored zone, preserving order.
|
||||
ASSERT_EQ(shadowZone->size(), 2);
|
||||
auto *shadowCard1 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(0));
|
||||
auto *shadowCard2 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(1));
|
||||
ASSERT_NE(shadowCard1, nullptr);
|
||||
ASSERT_NE(shadowCard2, nullptr);
|
||||
EXPECT_EQ(shadowCard1->getDataNode(), deckCard1);
|
||||
EXPECT_EQ(shadowCard2->getDataNode(), deckCard2);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// findGroupChild
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, FindGroupChildSkipsCustomZones)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
auto *group = new InnerDecklistNode("Creature", board);
|
||||
new DecklistModelSubZoneNode("Creature", board);
|
||||
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Creature"), group);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Missing"), nullptr);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(&root, DECK_ZONE_MAIN), board);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// sortWithCustomZonesLast
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsAscending)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
new DecklistModelSubZoneNode("Zebra", board);
|
||||
new InnerDecklistNode("Creature", board);
|
||||
new InnerDecklistNode("Instant", board);
|
||||
new DecklistModelSubZoneNode("Alpha", board);
|
||||
|
||||
root.setSortMethod(DeckSortMethod::ByName);
|
||||
|
||||
auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder);
|
||||
|
||||
// Groups sort first (by name), then custom zones (by name), always after groups.
|
||||
EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"}));
|
||||
|
||||
// Some non-identity movement occurred.
|
||||
EXPECT_FALSE(mapping.isEmpty());
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsDescending)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
new DecklistModelSubZoneNode("Zebra", board);
|
||||
new InnerDecklistNode("Creature", board);
|
||||
new InnerDecklistNode("Instant", board);
|
||||
new DecklistModelSubZoneNode("Alpha", board);
|
||||
|
||||
root.setSortMethod(DeckSortMethod::ByName);
|
||||
|
||||
(void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder);
|
||||
|
||||
// Groups still lead (descending), custom zones still last.
|
||||
EXPECT_EQ(childNames(board), (QStringList{"Instant", "Creature", "Zebra", "Alpha"}));
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, SortBoardMappingIsConsistent)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
|
||||
QList<AbstractDecklistNode *> originalOrder;
|
||||
auto *g0 = new InnerDecklistNode("Creature", board);
|
||||
originalOrder.append(g0);
|
||||
auto *z0 = new DecklistModelSubZoneNode("Zebra", board);
|
||||
originalOrder.append(z0);
|
||||
auto *g1 = new InnerDecklistNode("Instant", board);
|
||||
originalOrder.append(g1);
|
||||
auto *z1 = new DecklistModelSubZoneNode("Alpha", board);
|
||||
originalOrder.append(z1);
|
||||
|
||||
root.setSortMethod(DeckSortMethod::ByName);
|
||||
|
||||
auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder);
|
||||
|
||||
// The mapping reports, for each final row, the original row of the node now sitting there.
|
||||
ASSERT_EQ(mapping.size(), board->size());
|
||||
for (const auto &move : mapping) {
|
||||
const int preSortRow = move.first;
|
||||
const int finalRow = move.second;
|
||||
ASSERT_GE(preSortRow, 0);
|
||||
ASSERT_LT(preSortRow, originalOrder.size());
|
||||
EXPECT_EQ(board->at(finalRow), originalOrder[preSortRow]) << "row " << finalRow;
|
||||
}
|
||||
|
||||
// Final order sanity: groups first in name order, then custom zones.
|
||||
EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"}));
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones)
|
||||
{
|
||||
// A non-board node (e.g. a group whose children are cards) is sorted plainly;
|
||||
// custom zones are not a special case there. Cards sort by name.
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
auto *group = new InnerDecklistNode("Creature", board);
|
||||
cardNode(group, "Swords", 1);
|
||||
cardNode(group, "Bolt", 3);
|
||||
|
||||
root.setSortMethod(DeckSortMethod::ByName);
|
||||
|
||||
auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, group, Qt::AscendingOrder);
|
||||
EXPECT_EQ(childNames(group), (QStringList{"Bolt", "Swords"}));
|
||||
ASSERT_EQ(mapping.size(), 2);
|
||||
EXPECT_EQ(mapping[0].first, 1); // "Bolt" was originally at row 1
|
||||
EXPECT_EQ(mapping[0].second, 0);
|
||||
EXPECT_EQ(mapping[1].first, 0);
|
||||
EXPECT_EQ(mapping[1].second, 1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
283
tests/deck_list_model/deck_list_model_zone_integration_test.cpp
Normal file
283
tests/deck_list_model/deck_list_model_zone_integration_test.cpp
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int totalCustomZoneRows(const DeckListModel &model)
|
||||
{
|
||||
int count = 0;
|
||||
const int rootRows = model.rowCount(QModelIndex());
|
||||
for (int r = 0; r < rootRows; ++r) {
|
||||
const QModelIndex board = model.index(r, 0, QModelIndex());
|
||||
const int childRows = model.rowCount(board);
|
||||
for (int c = 0; c < childRows; ++c) {
|
||||
const QModelIndex child = model.index(c, 0, board);
|
||||
if (child.data(DeckRoles::IsCustomZoneRole).toBool()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
QModelIndex findBoardIndex(const DeckListModel &model, const QString &boardName)
|
||||
{
|
||||
for (int r = 0; r < model.rowCount(QModelIndex()); ++r) {
|
||||
const QModelIndex idx = model.index(r, 0, QModelIndex());
|
||||
if (idx.data(DeckRoles::IsCardRole).toBool()) {
|
||||
continue;
|
||||
}
|
||||
const QString name = idx.sibling(idx.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
if (name == boardName) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex findZoneRow(const DeckListModel &model, const QModelIndex &board)
|
||||
{
|
||||
for (int r = 0; r < model.rowCount(board); ++r) {
|
||||
const QModelIndex child = model.index(r, 0, board);
|
||||
if (child.data(DeckRoles::IsCustomZoneRole).toBool()) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The "Add to Zone" combobox/submenu lists getCustomZoneNames(), which reads the
|
||||
// deck tree. These verify the source data a freshly-created zone populates.
|
||||
|
||||
TEST(DeckListModelZoneIntegration, CreateZoneThenReadCustomZoneNames)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal"}));
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, CreateTwoZonesThenReadBoth)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr);
|
||||
EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"}));
|
||||
}
|
||||
|
||||
// Mirroring regression: rebuildTree must mirror each custom zone exactly once.
|
||||
TEST(DeckListModelZoneIntegration, RebuildTreeMirrorsEachZoneOnce)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// One direct mainboard card plus two nested custom zones.
|
||||
tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Swords to Plowshares", 1, "Removal", -1);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr);
|
||||
|
||||
model.rebuildTree();
|
||||
|
||||
EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"}));
|
||||
EXPECT_EQ(totalCustomZoneRows(model), 2);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Model behaviour: addCard routing, findCard lookup, removeRows guard, empty-zone survival.
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardRoutesIntoMirroredCustomZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
// The card is a direct child of the mirrored custom zone, not a new top-level zone.
|
||||
const QModelIndex zoneParent = added.parent();
|
||||
ASSERT_TRUE(zoneParent.isValid());
|
||||
EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Removal"));
|
||||
|
||||
// No "Removal" top-level zone appeared in the deck tree.
|
||||
auto *listRoot = tree->getRoot();
|
||||
bool topLevelRemoval = false;
|
||||
for (int i = 0; i < listRoot->size(); ++i) {
|
||||
if (auto *zone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i))) {
|
||||
topLevelRemoval |= zone->getName() == "Removal";
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(topLevelRemoval);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardToUnmirroredCustomZoneRebuildsNotCreatesTopLevel)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// The zone exists on the deck tree but the shadow tree has never mirrored it.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
const QModelIndex zoneParent = added.parent();
|
||||
ASSERT_TRUE(zoneParent.isValid());
|
||||
EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardCreatesGroupSeparatelyFromSameNamedZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// A custom zone named exactly like a grouping criterion.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Creature"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
CardInfoPtr bear = CardInfo::newInstance("Grizzly Bears");
|
||||
bear->setProperty(Mtg::MainCardType, "Creature");
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(bear), DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
// The card lands in a *group* node called "Creature", not swallowed by the custom zone.
|
||||
const QModelIndex groupParent = added.parent();
|
||||
ASSERT_TRUE(groupParent.isValid());
|
||||
EXPECT_FALSE(groupParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(groupParent.sibling(groupParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Creature"));
|
||||
|
||||
// The board keeps both rows: the "Creature" group and the "Creature" custom zone.
|
||||
const QModelIndex boardIndex = groupParent.parent();
|
||||
ASSERT_TRUE(boardIndex.isValid());
|
||||
EXPECT_EQ(model.rowCount(boardIndex), 2);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, FindCardResolvesCardInsideCustomZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
// findCard resolves through the card database; register the card we add.
|
||||
const QString cardName = "Swords to Plowshares";
|
||||
CardInfoPtr info = CardInfo::newInstance(cardName);
|
||||
CardDatabaseManager::getInstance()->addCard(info);
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(info), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
QModelIndex found = model.findCard(cardName, "Removal");
|
||||
EXPECT_TRUE(found.isValid());
|
||||
EXPECT_EQ(found, added);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, RemoveRowsRefusesCustomZoneRow)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1);
|
||||
model.rebuildTree();
|
||||
|
||||
const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(mainIndex.isValid());
|
||||
const QModelIndex zoneRow = findZoneRow(model, mainIndex);
|
||||
ASSERT_TRUE(zoneRow.isValid());
|
||||
|
||||
EXPECT_FALSE(model.removeRow(zoneRow.row(), zoneRow.parent()));
|
||||
EXPECT_EQ(model.rowCount(mainIndex), 2); // the zone survives, alongside the card group
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, EmptyCustomZoneSurvivesMirrorAndPruning)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// An empty custom zone must be mirrored (the stack deliberately keeps it alive).
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(mainIndex.isValid());
|
||||
EXPECT_EQ(model.rowCount(mainIndex), 1);
|
||||
EXPECT_TRUE(findZoneRow(model, mainIndex).isValid());
|
||||
}
|
||||
|
||||
// Regression: a board card named like the requested zone must not be mistaken for
|
||||
// a zone. Previously `findChild` matched any child by name, so a mainboard card
|
||||
// called "Lightning Bolt" made addCard believe a "Lightning Bolt" zone existed and
|
||||
// recurse through rebuildTree forever.
|
||||
TEST(DeckListModelZoneIntegration, AddCardToCardNamedZoneDoesNotRecurse)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1);
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Lightning Bolt");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
}
|
||||
|
||||
// Regression: adding to a custom zone that holds a nested sub-zone mirrored the
|
||||
// nested cards as flattened shadow rows, so the sorted shadow row index pointed
|
||||
// past the deck zone's direct children. The card must be appended to the deck
|
||||
// zone instead of being written out of range.
|
||||
TEST(DeckListModelZoneIntegration, AddCardToCustomZoneWithNestedSubZoneAppends)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
auto *removal = tree->addCustomZone(DECK_ZONE_MAIN, "Removal");
|
||||
ASSERT_NE(removal, nullptr);
|
||||
auto *deeper = new InnerDecklistNode("Deeper", removal);
|
||||
new DecklistCardNode("Lightning Bolt", 2, deeper, -1);
|
||||
model.rebuildTree();
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
ASSERT_TRUE(added.parent().data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
10
tests/deck_list_zones/CMakeLists.txt
Normal file
10
tests/deck_list_zones/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
add_executable(deck_list_zones_test deck_list_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_zones_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
deck_list_zones_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
add_test(NAME deck_list_zones_test COMMAND deck_list_zones_test)
|
||||
392
tests/deck_list_zones/deck_list_zones_test.cpp
Normal file
392
tests/deck_list_zones/deck_list_zones_test.cpp
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
/**
|
||||
* @file deck_list_zones_test.cpp
|
||||
* @brief Tests for custom deck zones (deck-unique zones nested under a board zone).
|
||||
*
|
||||
* Custom zones allow players to organize cards within a board (e.g. "Removal" under
|
||||
* the mainboard) without changing the board semantics: cards in a custom zone under
|
||||
* "main" are still mainboard cards for hashing, sideboard size, legality and export.
|
||||
*/
|
||||
|
||||
#include <QStringList>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Collects (board zone name, card node) pairs via forEachCard.
|
||||
*/
|
||||
struct BoardCardPair
|
||||
{
|
||||
QString boardZone;
|
||||
QString cardName;
|
||||
int amount;
|
||||
};
|
||||
|
||||
QList<BoardCardPair> collectBoardCardPairs(const DeckList &deck)
|
||||
{
|
||||
QList<BoardCardPair> result;
|
||||
deck.forEachCard([&result](InnerDecklistNode *boardZone, DecklistCardNode *card) {
|
||||
result.append({boardZone->getName(), card->getName(), card->getNumber()});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPair(const QList<BoardCardPair> &pairs, const QString &boardZone, const QString &cardName)
|
||||
{
|
||||
for (const auto &pair : pairs) {
|
||||
if (pair.boardZone == boardZone && pair.cardName == cardName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalCards(const QList<BoardCardPair> &pairs)
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &pair : pairs) {
|
||||
total += pair.amount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================================================
|
||||
// Zone creation
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, AddCustomZoneNestsUnderBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
auto *zone = tree->addCustomZone(DECK_ZONE_MAIN, "Removal");
|
||||
ASSERT_NE(zone, nullptr);
|
||||
EXPECT_EQ(zone->getName(), QString("Removal"));
|
||||
ASSERT_NE(zone->getParent(), nullptr);
|
||||
EXPECT_EQ(zone->getParent()->getName(), QString(DECK_ZONE_MAIN));
|
||||
|
||||
// The custom zone is nested, not a new top-level zone.
|
||||
auto topLevelZones = tree->getZoneNodes();
|
||||
QStringList topLevelNames;
|
||||
for (auto *node : topLevelZones) {
|
||||
topLevelNames.append(node->getName());
|
||||
}
|
||||
EXPECT_FALSE(topLevelNames.contains("Removal"));
|
||||
|
||||
// It is discoverable through the board zone.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
EXPECT_EQ(customZones.first()->getName(), QString("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, CustomZoneNamesAreDeckUnique)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
// Same name on a different board is rejected.
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_SIDE, "Removal"), nullptr);
|
||||
// A name that collides with a built-in board zone is rejected.
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAIN), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_SIDE), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAYBEBOARD), nullptr);
|
||||
EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_TOKENS), nullptr);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, AddCustomZoneUnknownBoardFails)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
EXPECT_EQ(tree->addCustomZone("not_a_board", "Removal"), nullptr);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MaybeboardIsLazilyCreated)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
|
||||
// The maybeboard board zone now exists, with the custom zone nested inside.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAYBEBOARD);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
EXPECT_EQ(customZones.first()->getName(), QString("Candidates"));
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Card placement
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, AddCardToCustomZoneKeepsBoardSemantics)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 4, "Removal", -1);
|
||||
|
||||
// The card is reported as a mainboard card.
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
|
||||
// It is physically nested inside the custom zone.
|
||||
auto customZones = tree->getCustomZones(DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(customZones.size(), 1);
|
||||
ASSERT_EQ(customZones.first()->size(), 1);
|
||||
auto *card = dynamic_cast<const DecklistCardNode *>(customZones.first()->at(0));
|
||||
ASSERT_NE(card, nullptr);
|
||||
EXPECT_EQ(card->getName(), QString("Lightning Bolt"));
|
||||
EXPECT_EQ(card->getNumber(), 4);
|
||||
|
||||
// Zone-scoped queries include it.
|
||||
EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).contains("Lightning Bolt"));
|
||||
EXPECT_FALSE(deck.getCardList({DECK_ZONE_SIDE}).contains("Lightning Bolt"));
|
||||
EXPECT_EQ(deck.getCardNodes({DECK_ZONE_MAIN}).size(), 1);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, LegacyTopLevelZoneStillWorks)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
// Unknown zone names create a legacy top-level zone (backwards compatibility).
|
||||
tree->addCard("Legacy Card", 2, "custom_legacy_zone", -1);
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, "custom_legacy_zone", "Legacy Card"));
|
||||
EXPECT_EQ(deck.getCardList({}).count("Legacy Card"), 1);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Zone management
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, RenameCustomZone)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->renameCustomZone("Removal", "Bolt Zone"));
|
||||
EXPECT_TRUE(hasPair(collectBoardCardPairs(deck), DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Bolt Zone"));
|
||||
|
||||
// Renaming to a taken name fails.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Other"), nullptr);
|
||||
EXPECT_FALSE(tree->renameCustomZone("Bolt Zone", "Other"));
|
||||
// Renaming a nonexistent zone fails.
|
||||
EXPECT_FALSE(tree->renameCustomZone("Ghost Zone", "Whatever"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MoveCustomZoneMovesCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE));
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
|
||||
// The custom zone is now nested under side.
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0);
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_SIDE).size(), 1);
|
||||
|
||||
// Moving to an unknown board fails.
|
||||
EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MoveCustomZoneFailsForUnknownBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board"));
|
||||
|
||||
// The zone is still under main.
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
}
|
||||
|
||||
// Regression: findCustomZoneByName walks every top-level zone, so a custom zone
|
||||
// an imported deck carries under a non-standard board (tokens) is still found and
|
||||
// movable. The pre-fix manager-level moveCustomZone only scanned the standard
|
||||
// boards and returned false for these with no feedback.
|
||||
TEST(DeckListZones, MoveCustomZoneNestedUnderTokensBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
auto *root = tree->getRoot();
|
||||
|
||||
auto *tokens = new InnerDecklistNode(DECK_ZONE_TOKENS, root);
|
||||
auto *removal = new InnerDecklistNode("Removal", tokens);
|
||||
new DecklistCardNode("Lightning Bolt", 2, removal, -1);
|
||||
|
||||
EXPECT_TRUE(tree->findCustomZoneByName("Removal"));
|
||||
EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE));
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_TOKENS, "Lightning Bolt"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, RemoveCustomZoneRemovesCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_TRUE(tree->removeCustomZone("Removal"));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0);
|
||||
EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).isEmpty());
|
||||
EXPECT_FALSE(tree->removeCustomZone("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, EmptyCustomZoneIsKeptOnCardDeletion)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
auto *card = tree->addCard("Lightning Bolt", 1, "Removal", -1);
|
||||
|
||||
// Deleting the last card must not delete the empty custom zone.
|
||||
EXPECT_TRUE(tree->deleteNode(card));
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Deck-wide behavior
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListZones, HashCountsCustomZoneCardsByBoard)
|
||||
{
|
||||
// Deck A: cards directly in main and side.
|
||||
DeckList direct;
|
||||
direct.addCard("Mountain", DECK_ZONE_MAIN);
|
||||
direct.addCard("Lightning Bolt", DECK_ZONE_MAIN);
|
||||
direct.addCard("Island", DECK_ZONE_SIDE);
|
||||
|
||||
// Deck B: identical, but organized in custom zones.
|
||||
DeckList organized;
|
||||
auto *tree = organized.getTree();
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Lands"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Mountain", 1, "Lands", -1);
|
||||
tree->addCard("Lightning Bolt", 1, "Removal", -1);
|
||||
tree->addCard("Island", 1, "Side Tech", -1);
|
||||
|
||||
EXPECT_EQ(direct.getDeckHash(), organized.getDeckHash());
|
||||
}
|
||||
|
||||
TEST(DeckListZones, SideboardSizeCountsCustomZoneCards)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Island", 3, "Side Tech", -1);
|
||||
tree->addCard("Forest", 2, DECK_ZONE_SIDE, -1);
|
||||
|
||||
EXPECT_EQ(deck.getSideboardSize(), 5);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, PlainExportIncludesMainAndSideCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
tree->addCard("Island", 1, "Side Tech", -1);
|
||||
|
||||
const QString plain = deck.writeToString_Plain(false, false);
|
||||
EXPECT_TRUE(plain.contains("2 Lightning Bolt"));
|
||||
EXPECT_TRUE(plain.contains("1 Island"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, PlainExportSkipsMaybeboardCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
tree->addCard("Wish Card", 4, "Candidates", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
const QString plain = deck.writeToString_Plain(false, false);
|
||||
EXPECT_FALSE(plain.contains("Wish Card"));
|
||||
EXPECT_TRUE(plain.contains("1 Mountain"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, NativeRoundTripPreservesCustomZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
tree->addCard("Island", 3, "Side Tech", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
// Round-trip through the native format.
|
||||
DeckList restored(deck.writeToString_Native());
|
||||
auto *restoredTree = restored.getTree();
|
||||
|
||||
EXPECT_EQ(restored.getDeckHash(), deck.getDeckHash());
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Removal"));
|
||||
EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_SIDE).size(), 1);
|
||||
|
||||
auto pairs = collectBoardCardPairs(restored);
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Island"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Mountain"));
|
||||
EXPECT_EQ(totalCards(pairs), 6);
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MaybeboardCustomZoneCardsAreExcludedFromHash)
|
||||
{
|
||||
// Maybeboard cards are editor-only and must never affect the deck hash.
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
tree->addCard("Wish Card", 4, "Candidates", -1);
|
||||
tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1);
|
||||
|
||||
DeckList expected;
|
||||
expected.addCard("Mountain", DECK_ZONE_MAIN);
|
||||
|
||||
EXPECT_EQ(deck.getDeckHash(), expected.getDeckHash());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
103
tests/lag_monitor_test.cpp
Normal file
103
tests/lag_monitor_test.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "client/lag_monitor.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QEvent>
|
||||
#include <QLoggingCategory>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/// Timestamps are taken at recording time; allow generous scheduler slack.
|
||||
constexpr qint64 TIMESTAMP_SLACK_MS = 10000;
|
||||
|
||||
} // namespace
|
||||
|
||||
class LagMonitorTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
LagMonitor monitor;
|
||||
};
|
||||
|
||||
TEST_F(LagMonitorTest, GapAtOrBelowThresholdIsIgnored)
|
||||
{
|
||||
monitor.recordGap(0);
|
||||
monitor.recordGap(LagMonitor::TICK_INTERVAL_MS);
|
||||
monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS);
|
||||
|
||||
EXPECT_TRUE(monitor.recentStalls().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, GapAboveThresholdIsRecorded)
|
||||
{
|
||||
monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1);
|
||||
|
||||
const QList<LagMonitor::StallRecord> stalls = monitor.recentStalls();
|
||||
ASSERT_EQ(1, stalls.size());
|
||||
EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 1, stalls.first().durationMs);
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, RecordedTimestampIsFresh)
|
||||
{
|
||||
monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1);
|
||||
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
ASSERT_EQ(1, monitor.recentStalls().size());
|
||||
EXPECT_LE(qAbs(monitor.recentStalls().first().timestampMsSinceEpoch - now), TIMESTAMP_SLACK_MS);
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, RecordsAreTrimmedToMaxOldestFirst)
|
||||
{
|
||||
for (int i = 0; i < LagMonitor::MAX_RECORDED_STALLS + 5; ++i) {
|
||||
monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1 + i);
|
||||
}
|
||||
|
||||
const QList<LagMonitor::StallRecord> stalls = monitor.recentStalls();
|
||||
ASSERT_EQ(LagMonitor::MAX_RECORDED_STALLS, stalls.size());
|
||||
EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 6, stalls.first().durationMs);
|
||||
EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 5 + LagMonitor::MAX_RECORDED_STALLS, stalls.last().durationMs);
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, GapAtPlausibilityCapIsKept)
|
||||
{
|
||||
monitor.recordGap(LagMonitor::MAX_PLAUSIBLE_STALL_MS);
|
||||
|
||||
ASSERT_EQ(1, monitor.recentStalls().size());
|
||||
EXPECT_EQ(LagMonitor::MAX_PLAUSIBLE_STALL_MS, monitor.recentStalls().first().durationMs);
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, GapBeyondPlausibilityCapIsDropped)
|
||||
{
|
||||
monitor.recordGap(LagMonitor::MAX_PLAUSIBLE_STALL_MS + 1);
|
||||
|
||||
EXPECT_TRUE(monitor.recentStalls().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, ClearStallsEmptiesList)
|
||||
{
|
||||
monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1);
|
||||
ASSERT_EQ(1, monitor.recentStalls().size());
|
||||
|
||||
monitor.clearStalls();
|
||||
|
||||
EXPECT_TRUE(monitor.recentStalls().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(LagMonitorTest, ApplicationStateChangeDoesNotRecordAStall)
|
||||
{
|
||||
QObject probe;
|
||||
QEvent event(QEvent::ApplicationStateChange);
|
||||
|
||||
QCoreApplication::sendEvent(&probe, &event);
|
||||
|
||||
EXPECT_TRUE(monitor.recentStalls().isEmpty());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QLoggingCategory::setFilterRules("lag_monitor.*=false");
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
149
tests/latency_tracker_test.cpp
Normal file
149
tests/latency_tracker_test.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/client/abstract/latency_tracker.h>
|
||||
|
||||
TEST(LatencyTrackerTest, EmptyTrackerYieldsZeroedStats)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
|
||||
const auto stats = tracker.stats();
|
||||
|
||||
EXPECT_EQ(0, stats.sampleCount);
|
||||
EXPECT_EQ(0, stats.lastMs);
|
||||
EXPECT_EQ(0, stats.medianMs);
|
||||
EXPECT_EQ(0, stats.p95Ms);
|
||||
EXPECT_EQ(0, stats.maxMs);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, SingleSampleIsEveryStatistic)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
tracker.addSample(42);
|
||||
|
||||
const auto stats = tracker.stats();
|
||||
|
||||
EXPECT_EQ(1, stats.sampleCount);
|
||||
EXPECT_EQ(42, stats.lastMs);
|
||||
EXPECT_EQ(42, stats.medianMs);
|
||||
EXPECT_EQ(42, stats.p95Ms);
|
||||
EXPECT_EQ(42, stats.maxMs);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, OddSizedWindowMedianAndP95)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (qint64 ms : {qint64(50), qint64(10), qint64(30), qint64(20), qint64(40)}) {
|
||||
tracker.addSample(ms);
|
||||
}
|
||||
|
||||
const auto stats = tracker.stats();
|
||||
|
||||
EXPECT_EQ(5, stats.sampleCount);
|
||||
EXPECT_EQ(40, stats.lastMs);
|
||||
EXPECT_EQ(30, stats.medianMs);
|
||||
// nearest-rank p95 of 5 samples: ceil(4.75) = 5th smallest
|
||||
EXPECT_EQ(50, stats.p95Ms);
|
||||
EXPECT_EQ(50, stats.maxMs);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, EvenSizedWindowMedianIsAverageOfMiddleTwo)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (qint64 ms : {qint64(10), qint64(20), qint64(30), qint64(40)}) {
|
||||
tracker.addSample(ms);
|
||||
}
|
||||
|
||||
const auto stats = tracker.stats();
|
||||
|
||||
EXPECT_EQ(4, stats.sampleCount);
|
||||
EXPECT_EQ(25, stats.medianMs);
|
||||
// nearest-rank p95 of 4 samples: ceil(3.8) = 4th smallest
|
||||
EXPECT_EQ(40, stats.p95Ms);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, WindowEvictsOldestSamples)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (int i = 0; i <= 99; ++i) {
|
||||
tracker.addSample(i);
|
||||
}
|
||||
|
||||
const auto stats = tracker.stats();
|
||||
|
||||
EXPECT_EQ(LatencyTracker::WindowSize, stats.sampleCount);
|
||||
EXPECT_EQ(99, stats.lastMs);
|
||||
EXPECT_EQ(99, stats.maxMs);
|
||||
// window now contains 36..99 (64 samples)
|
||||
EXPECT_EQ(67, stats.medianMs); // (67 + 68) / 2 with integer division
|
||||
EXPECT_EQ(96, stats.p95Ms); // ceil(0.95 * 64) - 1 = index 60 -> 36 + 60
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, ClearResetsAllState)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (int i = 0; i <= 99; ++i) {
|
||||
tracker.addSample(i);
|
||||
}
|
||||
|
||||
tracker.clear();
|
||||
const auto cleared = tracker.stats();
|
||||
EXPECT_EQ(0, cleared.sampleCount);
|
||||
|
||||
tracker.addSample(7);
|
||||
const auto stats = tracker.stats();
|
||||
EXPECT_EQ(1, stats.sampleCount);
|
||||
EXPECT_EQ(7, stats.lastMs);
|
||||
EXPECT_EQ(7, stats.medianMs);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, LastSampleSurvivesWraparound)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (int i = 0; i < LatencyTracker::WindowSize; ++i) {
|
||||
tracker.addSample(i);
|
||||
}
|
||||
tracker.addSample(1000);
|
||||
|
||||
EXPECT_EQ(1000, tracker.stats().lastMs);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, RecentSamplesAreChronologicalOldestFirst)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (qint64 ms : {qint64(50), qint64(10), qint64(30)}) {
|
||||
tracker.addSample(ms);
|
||||
}
|
||||
|
||||
const QList<qint64> samples = tracker.recentSamples();
|
||||
|
||||
EXPECT_EQ((QList<qint64>{50, 10, 30}), samples);
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, RecentSamplesFollowRingBufferWraparound)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
for (int i = 0; i <= 99; ++i) {
|
||||
tracker.addSample(i);
|
||||
}
|
||||
|
||||
const QList<qint64> samples = tracker.recentSamples();
|
||||
|
||||
ASSERT_EQ(LatencyTracker::WindowSize, samples.size());
|
||||
EXPECT_EQ(36, samples.first());
|
||||
EXPECT_EQ(99, samples.last());
|
||||
}
|
||||
|
||||
TEST(LatencyTrackerTest, RecentSamplesEmptyOnFreshAndClearedTracker)
|
||||
{
|
||||
LatencyTracker tracker;
|
||||
EXPECT_TRUE(tracker.recentSamples().isEmpty());
|
||||
|
||||
tracker.addSample(5);
|
||||
tracker.clear();
|
||||
EXPECT_TRUE(tracker.recentSamples().isEmpty());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
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();
|
||||
}
|
||||
83
tests/metrics_registry_test.cpp
Normal file
83
tests/metrics_registry_test.cpp
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <gtest/gtest.h>
|
||||
#include <metrics_registry.h>
|
||||
|
||||
TEST(MetricsRegistryTest, EmptyRegistryHasZeroedCounters)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
|
||||
EXPECT_EQ(0, registry.totalCommands());
|
||||
EXPECT_EQ(0, registry.totalTimeMs());
|
||||
EXPECT_EQ(0, registry.activeTypeCount());
|
||||
EXPECT_EQ(0, registry.getGameStartSnapshot().count);
|
||||
}
|
||||
|
||||
TEST(MetricsRegistryTest, SampleIsRecordedInTotalsAndSlot)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), 7);
|
||||
|
||||
EXPECT_EQ(1, registry.totalCommands());
|
||||
EXPECT_EQ(7, registry.totalTimeMs());
|
||||
EXPECT_EQ(1, registry.activeTypeCount());
|
||||
|
||||
const auto stats = registry.collectActiveStats();
|
||||
ASSERT_EQ(1, stats.size());
|
||||
EXPECT_EQ(MetricsRegistry::typeIdFor(0, 1000), stats[0].typeId);
|
||||
EXPECT_EQ(1, stats[0].count);
|
||||
EXPECT_EQ(7, stats[0].totalMs);
|
||||
}
|
||||
|
||||
TEST(MetricsRegistryTest, KindEncodingSeparatesSameExtensionNumber)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
const int sessionPing = MetricsRegistry::typeIdFor(0, 1000);
|
||||
const int roomLeaveRoom = MetricsRegistry::typeIdFor(1, 1000);
|
||||
ASSERT_NE(sessionPing, roomLeaveRoom);
|
||||
|
||||
registry.observeCommand(sessionPing, 1);
|
||||
registry.observeCommand(roomLeaveRoom, 5000);
|
||||
|
||||
EXPECT_EQ(2, registry.activeTypeCount());
|
||||
}
|
||||
|
||||
TEST(MetricsRegistryTest, OutOfRangeIdsLandInOverflowSlot)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
registry.observeCommand(-1, 4);
|
||||
registry.observeCommand(MetricsRegistry::MaxTypes + 12345, 4);
|
||||
|
||||
EXPECT_EQ(2, registry.totalCommands());
|
||||
EXPECT_EQ(1, registry.activeTypeCount()); // both collapsed into one slot
|
||||
EXPECT_EQ(8, registry.totalTimeMs());
|
||||
}
|
||||
|
||||
TEST(MetricsRegistryTest, NegativeDurationsAreClamped)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), -50);
|
||||
|
||||
EXPECT_EQ(0, registry.totalTimeMs());
|
||||
}
|
||||
|
||||
TEST(MetricsRegistryTest, GameStartTrackedSeparatelyFromCommands)
|
||||
{
|
||||
MetricsRegistry registry;
|
||||
registry.observeGameStartDurationMs(120);
|
||||
|
||||
EXPECT_EQ(0, registry.totalCommands());
|
||||
EXPECT_EQ(0, registry.totalTimeMs());
|
||||
EXPECT_EQ(0, registry.activeTypeCount());
|
||||
|
||||
const auto snapshot = registry.getGameStartSnapshot();
|
||||
EXPECT_EQ(1, snapshot.count);
|
||||
EXPECT_EQ(120, snapshot.totalMs);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#include "game/game_config.h"
|
||||
#include "game/server_abstract_player.h"
|
||||
#include "game/server_card.h"
|
||||
#include "game/server_cardzone.h"
|
||||
|
|
@ -22,7 +23,21 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest)
|
|||
// instantiate a fake server instance
|
||||
FakeServer server;
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
Server_Game game(user, 1, "", "", 2, QList<int>(), false, false, false, false, false, false, 20, false, &room);
|
||||
GameConfig config{.creatorInfo = user,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = QString(),
|
||||
.maxPlayers = 2,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = false,
|
||||
.onlyRegistered = false,
|
||||
.spectatorsAllowed = false,
|
||||
.spectatorsNeedPassword = false,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false};
|
||||
Server_Game game(config, &room);
|
||||
Server_AbstractPlayer player(&game, 1, user, false, nullptr);
|
||||
Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone);
|
||||
Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone);
|
||||
|
|
|
|||
|
|
@ -7,3 +7,63 @@ endif()
|
|||
target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
||||
|
||||
add_test(NAME parse_cipt_test COMMAND parse_cipt_test)
|
||||
|
||||
# Oracle importer unit tests
|
||||
add_executable(
|
||||
oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_test.cpp
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME oracle_importer_test COMMAND oracle_importer_test)
|
||||
|
||||
# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark)
|
||||
# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark
|
||||
# can download and decompress whatever AllPrintings format the default URL selects.
|
||||
find_package(ZLIB)
|
||||
if(ZLIB_FOUND)
|
||||
add_definitions("-DHAS_ZLIB")
|
||||
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
||||
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "Oracle tests: zlib not found; zip download benchmark disabled")
|
||||
endif()
|
||||
|
||||
find_package(LibLZMA)
|
||||
if(LIBLZMA_FOUND)
|
||||
add_definitions("-DHAS_LZMA")
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
||||
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled")
|
||||
endif()
|
||||
|
||||
add_executable(
|
||||
oracle_importer_benchmark_test
|
||||
${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES}
|
||||
)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(oracle_importer_benchmark_test gtest)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
oracle_importer_benchmark_test
|
||||
libcockatrice_card
|
||||
libcockatrice_interfaces
|
||||
Threads::Threads
|
||||
${GTEST_BOTH_LIBRARIES}
|
||||
${TEST_QT_MODULES}
|
||||
${_ORACLE_BENCH_EXTRA_LIBRARIES}
|
||||
)
|
||||
|
|
|
|||
578
tests/oracle/oracle_importer_benchmark_test.cpp
Normal file
578
tests/oracle/oracle_importer_benchmark_test.cpp
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
#include "../../oracle/src/oracleimporter.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QBuffer>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
#if defined(HAS_LZMA)
|
||||
#include "../../oracle/src/lzma/decompress.h"
|
||||
#endif
|
||||
#if defined(HAS_ZLIB)
|
||||
#include "../../oracle/src/zip/unzip.h"
|
||||
#endif
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include <mach/mach.h>
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
|
||||
// Helper: build a synthetic MTGJSON-style JSON with the given number of sets and cards per set
|
||||
static QByteArray buildSyntheticData(int numSets, int cardsPerSet)
|
||||
{
|
||||
QJsonObject dataObj;
|
||||
for (int s = 0; s < numSets; ++s) {
|
||||
QJsonArray cardsArray;
|
||||
for (int c = 0; c < cardsPerSet; ++c) {
|
||||
QJsonObject card;
|
||||
card["name"] = QString("Card %1").arg(s * cardsPerSet + c);
|
||||
card["text"] = "This is a test card with some rules text.";
|
||||
card["layout"] = "normal";
|
||||
card["manaCost"] = "{W}";
|
||||
card["type"] = "Creature — Human";
|
||||
card["power"] = "2";
|
||||
card["toughness"] = "2";
|
||||
card["colors"] = QJsonArray{"W"};
|
||||
card["colorIdentity"] = QJsonArray{"W"};
|
||||
card["types"] = QJsonArray{"Creature"};
|
||||
// Real MTGJSON types: floats and booleans, not strings. This
|
||||
// exercises the QVariant coercion in the property reader.
|
||||
card["convertedManaCost"] = 1.0;
|
||||
card["manaValue"] = 1.0;
|
||||
card["isOnlineOnly"] = false;
|
||||
card["isRebalanced"] = false;
|
||||
|
||||
QJsonObject legalities;
|
||||
legalities["standard"] = "legal";
|
||||
legalities["modern"] = "legal";
|
||||
legalities["legacy"] = "legal";
|
||||
legalities["vintage"] = "legal";
|
||||
legalities["commander"] = "legal";
|
||||
card["legalities"] = legalities;
|
||||
|
||||
QJsonObject identifiers;
|
||||
identifiers["scryfallId"] = QString("id-%1-%2").arg(s).arg(c);
|
||||
card["identifiers"] = identifiers;
|
||||
|
||||
// In AllPrintings, number and rarity are flat fields on the card
|
||||
// object, exactly as set below.
|
||||
card["number"] = QString::number(c + 1);
|
||||
card["rarity"] = "common";
|
||||
|
||||
cardsArray.append(card);
|
||||
}
|
||||
|
||||
QJsonObject setObj;
|
||||
setObj["code"] = QString("T%1").arg(s, 2, 10, QChar('0'));
|
||||
setObj["name"] = QString("Test Set %1").arg(s);
|
||||
setObj["type"] = "expansion";
|
||||
setObj["releaseDate"] = "2024-01-01";
|
||||
setObj["cards"] = cardsArray;
|
||||
|
||||
dataObj[QString("T%1").arg(s, 2, 10, QChar('0'))] = setObj;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = dataObj;
|
||||
return QJsonDocument(root).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Import throughput benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, ImportThroughput)
|
||||
{
|
||||
static constexpr int numSets = 10;
|
||||
static constexpr int cardsPerSet = 500;
|
||||
|
||||
QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
OracleImporter importer;
|
||||
|
||||
// Phase 1: Parse JSON
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
bool ok = importer.readSetsFromByteArray(data);
|
||||
ASSERT_TRUE(ok);
|
||||
qint64 parseMs = timer.elapsed();
|
||||
|
||||
// Phase 2: Import cards
|
||||
timer.restart();
|
||||
int importedSets = importer.startImport();
|
||||
qint64 importMs = timer.elapsed();
|
||||
|
||||
int totalImported = 0;
|
||||
for (const auto &card : importer.getCardList()) {
|
||||
Q_UNUSED(card);
|
||||
totalImported++;
|
||||
}
|
||||
|
||||
// The fixture generates globally unique card names, so the expected
|
||||
// counts are exact: a regression here means cards were dropped.
|
||||
ASSERT_EQ(importedSets, numSets);
|
||||
ASSERT_EQ(totalImported, numSets * cardsPerSet);
|
||||
// Real-data probe: numeric convertedManaCost must be coerced to text
|
||||
// (regression for the QJsonValue::toString() reader in #7214).
|
||||
auto probeCard = importer.getCardList().value("Card 0");
|
||||
ASSERT_FALSE(probeCard.isNull());
|
||||
ASSERT_EQ(probeCard->getProperty("cmc"), "1");
|
||||
|
||||
qDebug().noquote()
|
||||
<< QString("Oracle Import Benchmark: %1 sets, %2 unique cards").arg(importedSets).arg(totalImported);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
qDebug().noquote() << QString(" Total: %1 ms").arg(parseMs + importMs);
|
||||
if (importMs > 0) {
|
||||
qDebug().noquote() << QString(" Throughput: %1 cards/sec")
|
||||
.arg(static_cast<double>(totalImported) / importMs * 1000.0, 0, 'f', 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// readSetsFromByteArray benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, ParseJsonThroughput)
|
||||
{
|
||||
static constexpr int numSets = 20;
|
||||
static constexpr int cardsPerSet = 1000;
|
||||
|
||||
QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
// Run 5 iterations and report average
|
||||
static constexpr int iterations = 5;
|
||||
qint64 totalMs = 0;
|
||||
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
OracleImporter importer;
|
||||
QByteArray source = data;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
bool ok = importer.readSetsFromByteArray(std::move(source));
|
||||
ASSERT_TRUE(ok);
|
||||
totalMs += timer.elapsed();
|
||||
}
|
||||
|
||||
qint64 avgMs = totalMs / iterations;
|
||||
qDebug().noquote() << QString("Parse Benchmark (%1 iterations): avg %2 ms for %3 sets x %4 cards")
|
||||
.arg(iterations)
|
||||
.arg(avgMs)
|
||||
.arg(numSets)
|
||||
.arg(cardsPerSet);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Split card merging benchmark
|
||||
// ============================================================================
|
||||
|
||||
TEST(OracleBenchmark, SplitCardMerging)
|
||||
{
|
||||
static constexpr int numSplitCards = 1000;
|
||||
|
||||
QJsonArray cardsList;
|
||||
for (int i = 0; i < numSplitCards; ++i) {
|
||||
QJsonObject face1;
|
||||
face1["name"] = QString("Fire %1 // Ice %1").arg(i);
|
||||
face1["text"] = "Fire side text.";
|
||||
face1["layout"] = "split";
|
||||
face1["side"] = "a";
|
||||
face1["faceName"] = QString("Fire %1").arg(i);
|
||||
face1["colors"] = QJsonArray{"R"};
|
||||
face1["colorIdentity"] = QJsonArray{"R"};
|
||||
face1["types"] = QJsonArray{"Instant"};
|
||||
face1["manaCost"] = "{R}";
|
||||
face1["legalities"] = QJsonObject{{"standard", "not_legal"}};
|
||||
face1["identifiers"] = QJsonObject{{"scryfallId", QString("f-%1").arg(i)}};
|
||||
face1["number"] = QString::number(i + 1);
|
||||
face1["rarity"] = "uncommon";
|
||||
|
||||
QJsonObject face2;
|
||||
face2["name"] = QString("Fire %1 // Ice %1").arg(i);
|
||||
face2["text"] = "Ice side text.";
|
||||
face2["layout"] = "split";
|
||||
face2["side"] = "b";
|
||||
face2["faceName"] = QString("Ice %1").arg(i);
|
||||
face2["colors"] = QJsonArray{"U"};
|
||||
face2["colorIdentity"] = QJsonArray{"U"};
|
||||
face2["types"] = QJsonArray{"Instant"};
|
||||
face2["manaCost"] = "{U}";
|
||||
face2["legalities"] = QJsonObject{{"standard", "not_legal"}};
|
||||
face2["identifiers"] = QJsonObject{{"scryfallId", QString("i-%1").arg(i)}};
|
||||
face2["number"] = QString::number(i + 1);
|
||||
face2["rarity"] = "uncommon";
|
||||
|
||||
cardsList.append(face1);
|
||||
cardsList.append(face2);
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
CardSetPtr set = CardSet::newInstance(&controller, "TST", "Split Test");
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
int count = importer.importCardsFromSet(set, cardsList);
|
||||
qint64 ms = timer.elapsed();
|
||||
|
||||
ASSERT_EQ(count, numSplitCards);
|
||||
qDebug().noquote() << QString("Split Card Merge Benchmark: %1 cards in %2 ms (%3 cards/sec)")
|
||||
.arg(count)
|
||||
.arg(ms)
|
||||
.arg(ms > 0 ? static_cast<double>(count) / ms * 1000.0 : 0.0, 0, 'f', 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// sortAndReduceColors microbenchmark
|
||||
// ============================================================================
|
||||
|
||||
// We can't call sortAndReduceColors directly (it's static), so we benchmark
|
||||
// through importCardsFromSet with color properties.
|
||||
|
||||
TEST(OracleBenchmark, ImportCardsWithColors)
|
||||
{
|
||||
static constexpr int numCards = 10000;
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
CardSetPtr set = CardSet::newInstance(&controller, "TST", "Color Test");
|
||||
|
||||
QJsonArray cardsList;
|
||||
for (int i = 0; i < numCards; ++i) {
|
||||
QJsonObject card;
|
||||
card["name"] = QString("Color Card %1").arg(i);
|
||||
card["text"] = "Rules text.";
|
||||
card["layout"] = "normal";
|
||||
card["manaCost"] = "{W}";
|
||||
card["type"] = "Creature — Human";
|
||||
card["types"] = QJsonArray{"Creature"};
|
||||
card["colors"] = QJsonArray{"B", "R", "G", "W", "U"};
|
||||
card["colorIdentity"] = QJsonArray{"B", "R", "G", "W", "U"};
|
||||
card["number"] = QString::number(i + 1);
|
||||
card["rarity"] = "common";
|
||||
card["legalities"] = QJsonObject{{"standard", "legal"}};
|
||||
card["identifiers"] = QJsonObject{{"scryfallId", QString("c-%1").arg(i)}};
|
||||
cardsList.append(card);
|
||||
}
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
int count = importer.importCardsFromSet(set, cardsList);
|
||||
qint64 ms = timer.elapsed();
|
||||
|
||||
ASSERT_EQ(count, numCards);
|
||||
qDebug().noquote() << QString("Import with Colors Benchmark: %1 cards in %2 ms (%3 cards/sec)")
|
||||
.arg(count)
|
||||
.arg(ms)
|
||||
.arg(ms > 0 ? static_cast<double>(count) / ms * 1000.0 : 0.0, 0, 'f', 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAM usage measurement
|
||||
// ============================================================================
|
||||
|
||||
// Mirrors the default AllPrintings URL selection in oracle/src/pages.cpp.
|
||||
#if defined(HAS_LZMA)
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.xz");
|
||||
#elif defined(HAS_ZLIB)
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.zip");
|
||||
#else
|
||||
static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json");
|
||||
#endif
|
||||
|
||||
// Magic bytes also from oracle/src/pages.cpp
|
||||
static const QByteArray kXzSignature("\xFD\x37\x7A\x58\x5A", 6);
|
||||
static const QByteArray kZipSignature("PK");
|
||||
|
||||
struct MemorySnapshot
|
||||
{
|
||||
qint64 peakRssKb = -1; // process high-water mark (VmHWM on Linux, ru_maxrss on macOS)
|
||||
qint64 rssKb = -1; // current resident set size
|
||||
bool available = false;
|
||||
|
||||
static MemorySnapshot current()
|
||||
{
|
||||
MemorySnapshot snap;
|
||||
#if defined(Q_OS_LINUX)
|
||||
QFile statusFile("/proc/self/status");
|
||||
if (statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
// /proc files report size() == 0, so atEnd() is immediately true: read everything first.
|
||||
const QList<QByteArray> lines = statusFile.readAll().split('\n');
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.startsWith("VmHWM:")) {
|
||||
snap.peakRssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong();
|
||||
} else if (line.startsWith("VmRSS:")) {
|
||||
snap.rssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong();
|
||||
}
|
||||
}
|
||||
snap.available = snap.peakRssKb >= 0;
|
||||
}
|
||||
#elif defined(Q_OS_MACOS)
|
||||
struct rusage usage;
|
||||
if (getrusage(RUSAGE_SELF, &usage) == 0) {
|
||||
snap.peakRssKb = usage.ru_maxrss / 1024; // bytes -> kB
|
||||
snap.available = snap.peakRssKb >= 0;
|
||||
}
|
||||
// getrusage has no current-RSS equivalent; task_info's resident_size
|
||||
// is the closest macOS analog to Linux VmRSS.
|
||||
mach_task_basic_info info = {};
|
||||
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) ==
|
||||
KERN_SUCCESS) {
|
||||
snap.rssKb = info.resident_size / 1024;
|
||||
}
|
||||
#endif
|
||||
return snap;
|
||||
}
|
||||
};
|
||||
|
||||
static QString formatKb(qint64 kb)
|
||||
{
|
||||
if (kb < 0) {
|
||||
return "N/A";
|
||||
}
|
||||
return QString("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
|
||||
}
|
||||
|
||||
static void logRamPhase(const QString &phase, const MemorySnapshot &baseline, const MemorySnapshot ¤t)
|
||||
{
|
||||
if (!baseline.available || !current.available) {
|
||||
qDebug().noquote() << QString(" %1: memory stats unavailable on this platform").arg(phase);
|
||||
return;
|
||||
}
|
||||
// VmHWM / ru_maxrss are monotonically non-decreasing high-water marks, so a
|
||||
// peak-based delta between phases is ~0.0 MB by construction once the
|
||||
// fixture build has set the process peak. The live signals are current RSS
|
||||
// and the process peak; the delta is meaningful only where the baseline was
|
||||
// taken immediately before the phase it measures (e.g. the import phase,
|
||||
// which compares afterParse against afterImport).
|
||||
QString rssDelta = "N/A";
|
||||
if (current.rssKb >= 0 && baseline.rssKb >= 0) {
|
||||
rssDelta = formatKb(current.rssKb - baseline.rssKb);
|
||||
}
|
||||
qDebug().noquote() << QString(" %1: current RSS %2 | delta vs baseline %3 | process peak %4")
|
||||
.arg(phase)
|
||||
.arg(formatKb(current.rssKb))
|
||||
.arg(rssDelta)
|
||||
.arg(formatKb(current.peakRssKb));
|
||||
}
|
||||
|
||||
// Decompresses the download payload when the default URL is a compressed build,
|
||||
// mirroring the wizard's magic-byte handling in oracle/src/pages.cpp.
|
||||
static QByteArray decompressSetsData(const QByteArray &payload)
|
||||
{
|
||||
if (payload.startsWith(kXzSignature)) {
|
||||
#if defined(HAS_LZMA)
|
||||
QBuffer inBuffer(const_cast<QByteArray *>(&payload));
|
||||
QByteArray out;
|
||||
QBuffer outBuffer(&out);
|
||||
inBuffer.open(QIODevice::ReadOnly);
|
||||
outBuffer.open(QIODevice::WriteOnly);
|
||||
XzDecompressor xz;
|
||||
if (!xz.decompress(&inBuffer, &outBuffer)) {
|
||||
qDebug() << "RAM benchmark: xz decompression failed";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
#else
|
||||
qDebug() << "RAM benchmark: download is xz-compressed but this build has no LZMA support";
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
if (payload.startsWith(kZipSignature)) {
|
||||
#if defined(HAS_ZLIB)
|
||||
QBuffer inBuffer(const_cast<QByteArray *>(&payload));
|
||||
inBuffer.open(QIODevice::ReadOnly);
|
||||
UnZip unzip;
|
||||
if (unzip.openArchive(&inBuffer) != UnZip::Ok) {
|
||||
qDebug() << "RAM benchmark: zip archive open failed";
|
||||
return {};
|
||||
}
|
||||
if (unzip.fileList().size() != 1) {
|
||||
qDebug() << "RAM benchmark: zip archive doesn't contain exactly one file";
|
||||
return {};
|
||||
}
|
||||
QByteArray out;
|
||||
QBuffer outBuffer(&out);
|
||||
outBuffer.open(QIODevice::WriteOnly);
|
||||
const auto errorCode = unzip.extractFile(unzip.fileList().value(0), &outBuffer);
|
||||
unzip.closeArchive();
|
||||
if (errorCode != UnZip::Ok) {
|
||||
qDebug() << "RAM benchmark: zip extraction failed";
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
#else
|
||||
qDebug() << "RAM benchmark: download is zip-compressed but this build has no zlib support";
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
TEST(OracleBenchmark, ImportRamUsage)
|
||||
{
|
||||
static constexpr int numSets = 30;
|
||||
static constexpr int cardsPerSet = 2000; // ~60k cards, roughly AllPrintings scale
|
||||
|
||||
// Baseline must precede the fixture build: a high-water mark set while
|
||||
// generating the synthetic JSON would otherwise mask the importer phases.
|
||||
// Where memory stats are unavailable (Windows), skip before doing the
|
||||
// 60k-card fixture build, which would otherwise be pure wasted work.
|
||||
const MemorySnapshot baseline = MemorySnapshot::current();
|
||||
if (!baseline.available) {
|
||||
GTEST_SKIP() << "Memory stats unavailable on this platform";
|
||||
}
|
||||
|
||||
const QByteArray data = buildSyntheticData(numSets, cardsPerSet);
|
||||
|
||||
// The fixture build leaves freed-but-unreturned arenas behind (current RSS
|
||||
// rarely falls once glibc allocates). Baseline immediately after it so the
|
||||
// parse phase measures only the importer's own growth (~40 MB) rather than
|
||||
// swallowing the fixture builder's spike.
|
||||
const MemorySnapshot afterFixture = MemorySnapshot::current();
|
||||
logRamPhase("fixture build", baseline, afterFixture);
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
ASSERT_TRUE(importer.readSetsFromByteArray(std::move(data)));
|
||||
const qint64 parseMs = timer.elapsed();
|
||||
const MemorySnapshot afterParse = MemorySnapshot::current();
|
||||
|
||||
timer.restart();
|
||||
const int importedSets = importer.startImport();
|
||||
const qint64 importMs = timer.elapsed();
|
||||
const MemorySnapshot afterImport = MemorySnapshot::current();
|
||||
|
||||
importer.releaseSetData();
|
||||
const MemorySnapshot afterRelease = MemorySnapshot::current();
|
||||
|
||||
const int totalCards = importer.getCardList().size();
|
||||
qDebug().noquote() << QString("Oracle RAM Benchmark (synthetic): %1 sets, %2 cards, %3 MB JSON")
|
||||
.arg(importedSets)
|
||||
.arg(totalCards)
|
||||
.arg(data.size() / (1024.0 * 1024.0), 0, 'f', 1);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
logRamPhase("parse", afterFixture, afterParse);
|
||||
logRamPhase("import", afterParse, afterImport);
|
||||
logRamPhase("after releaseSetData()", afterImport, afterRelease);
|
||||
|
||||
// Freeing the parsed tree rarely moves current RSS (allocator reuse), so the
|
||||
// meaningful signal that release actually dropped the buffers is emptiness,
|
||||
// not an RSS delta.
|
||||
ASSERT_TRUE(importer.getSets().isEmpty());
|
||||
}
|
||||
|
||||
TEST(OracleBenchmark, ImportRamUsageAllPrintings)
|
||||
{
|
||||
// Only "1" enables the download: unset (the default and the CI setup) and
|
||||
// an explicit "0" both disable it.
|
||||
bool envOk = false;
|
||||
const int enabled = qEnvironmentVariableIntValue("COCKATRICE_ORACLE_RAM_BENCHMARK", &envOk);
|
||||
if (!envOk || enabled == 0) {
|
||||
GTEST_SKIP() << "Set COCKATRICE_ORACLE_RAM_BENCHMARK=1 to download the real AllPrintings dataset for this "
|
||||
"RAM benchmark. Default URL: "
|
||||
<< kDefaultAllPrintingsUrl.toDisplayString().toStdString();
|
||||
}
|
||||
|
||||
// Baseline must precede the request so the phase covers the download +
|
||||
// decompress step, including the payload materialized by readAll().
|
||||
const MemorySnapshot baseline = MemorySnapshot::current();
|
||||
if (!baseline.available) {
|
||||
GTEST_SKIP() << "Memory stats unavailable on this platform";
|
||||
}
|
||||
|
||||
QNetworkAccessManager nam;
|
||||
QNetworkRequest request(kDefaultAllPrintingsUrl);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, "Cockatrice Oracle RAM benchmark");
|
||||
QNetworkReply *reply = nam.get(request);
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer timeoutTimer;
|
||||
timeoutTimer.setSingleShot(true);
|
||||
bool timedOut = false;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, [&] {
|
||||
timedOut = true;
|
||||
reply->abort();
|
||||
});
|
||||
timeoutTimer.start(10 * 60 * 1000);
|
||||
loop.exec();
|
||||
timeoutTimer.stop();
|
||||
|
||||
// abort() leaves reply->error() as OperationCanceledError, so a timed-out
|
||||
// download takes the same GTEST_SKIP path as any other network error
|
||||
// instead of reading a truncated body and failing the parse below.
|
||||
if (timedOut || reply->error() != QNetworkReply::NoError) {
|
||||
GTEST_SKIP() << "Download failed: " << reply->errorString().toStdString();
|
||||
}
|
||||
const QByteArray payload = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
// mtgjson can answer 200 with an HTML page (mirrors the wizard's '<' check
|
||||
// in pages.cpp); reject it before trying to decompress/parse.
|
||||
if (payload.startsWith("<")) {
|
||||
GTEST_SKIP() << "Download returned a non-JSON body (HTML page instead of data), skipping";
|
||||
}
|
||||
|
||||
const QByteArray setsData = decompressSetsData(payload);
|
||||
const MemorySnapshot afterDownload = MemorySnapshot::current();
|
||||
if (setsData.isEmpty()) {
|
||||
GTEST_SKIP() << "No data to import (download or decompression failed)";
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController controller;
|
||||
OracleImporter importer;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
ASSERT_TRUE(importer.readSetsFromByteArray(std::move(setsData)));
|
||||
const qint64 parseMs = timer.elapsed();
|
||||
const MemorySnapshot afterParse = MemorySnapshot::current();
|
||||
|
||||
timer.restart();
|
||||
const int importedSets = importer.startImport();
|
||||
const qint64 importMs = timer.elapsed();
|
||||
const MemorySnapshot afterImport = MemorySnapshot::current();
|
||||
|
||||
importer.releaseSetData();
|
||||
const MemorySnapshot afterRelease = MemorySnapshot::current();
|
||||
|
||||
const int totalCards = importer.getCardList().size();
|
||||
qDebug().noquote() << QString("Oracle RAM Benchmark (real AllPrintings): %1 sets, %2 unique cards")
|
||||
.arg(importedSets)
|
||||
.arg(totalCards);
|
||||
qDebug().noquote() << QString(" URL: %1").arg(kDefaultAllPrintingsUrl.toDisplayString());
|
||||
qDebug().noquote() << QString(" Downloaded: %1 MB, decompressed: %2 MB")
|
||||
.arg(payload.size() / (1024.0 * 1024.0), 0, 'f', 1)
|
||||
.arg(setsData.size() / (1024.0 * 1024.0), 0, 'f', 1);
|
||||
qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs);
|
||||
qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs);
|
||||
logRamPhase("download+decompress", baseline, afterDownload);
|
||||
logRamPhase("parse", afterDownload, afterParse);
|
||||
logRamPhase("import", afterParse, afterImport);
|
||||
logRamPhase("after releaseSetData()", afterImport, afterRelease);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Required for the event loop used by the real-AllPrintings download benchmark
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
1128
tests/oracle/oracle_importer_test.cpp
Normal file
1128
tests/oracle/oracle_importer_test.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,9 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <cstring>
|
||||
#include <libcockatrice/utility/passwordhasher.h>
|
||||
|
||||
RNG_Abstract *rng;
|
||||
|
||||
namespace
|
||||
{
|
||||
class PasswordHashTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
rng = new RNG_SFMT;
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete rng;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(PasswordHashTest, RegressionTest)
|
||||
{
|
||||
|
|
@ -29,6 +13,29 @@ TEST(PasswordHashTest, RegressionTest)
|
|||
QString hash = PasswordHasher::computeHash(password, salt);
|
||||
ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same";
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, SaltUsesAlphanumericCharset)
|
||||
{
|
||||
static const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const QString salt = PasswordHasher::generateRandomSalt();
|
||||
ASSERT_EQ(salt.size(), 16);
|
||||
for (const QChar &c : salt) {
|
||||
ASSERT_NE(strchr(alphanum, c.toLatin1()), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, SaltsAreUnique)
|
||||
{
|
||||
const QString salt1 = PasswordHasher::generateRandomSalt();
|
||||
const QString salt2 = PasswordHasher::generateRandomSalt();
|
||||
ASSERT_NE(salt1, salt2);
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, TokenHasExpectedLength)
|
||||
{
|
||||
const QString token = PasswordHasher::generateActivationToken();
|
||||
ASSERT_EQ(token.size(), 16);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
126
tests/playmat_resolver_test.cpp
Normal file
126
tests/playmat_resolver_test.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
PlaymatInfo makePlaymatInfo(const QString &name, const QString &providerId = QString())
|
||||
{
|
||||
PlaymatInfo info;
|
||||
info.card = {name, providerId};
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PlaymatResolverTest, EmptyChainReturnsEmpty)
|
||||
{
|
||||
DeckList deck;
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_TRUE(resolved.card.isEmpty());
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {}});
|
||||
|
||||
const PlaymatInfo force = makePlaymatInfo(QStringLiteral("Force Mat"), QStringLiteral("force-provider"));
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Force Mat"));
|
||||
EXPECT_EQ(resolved.card.providerId, QStringLiteral("force-provider"));
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, DeckWinsOverFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {0.1, 0.2, 0.3, 1.5}});
|
||||
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat"));
|
||||
EXPECT_EQ(resolved.card.providerId, QStringLiteral("deck-provider"));
|
||||
EXPECT_DOUBLE_EQ(resolved.params.marginPctL, 0.1);
|
||||
EXPECT_DOUBLE_EQ(resolved.params.verticalOffset, 0.3);
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, FallbackUsedWhenDeckHasNone)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Fallback Mat"));
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, FixedAlwaysUsesFirst)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second"))};
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, i);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("First"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RoundRobinCyclesAndWraps)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second")),
|
||||
makePlaymatInfo(QStringLiteral("Third"))};
|
||||
|
||||
const QStringList expected = {QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"),
|
||||
QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third")};
|
||||
for (int i = 0; i < expected.size(); ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, i);
|
||||
EXPECT_EQ(resolved.card.name, expected.at(i));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RoundRobinRespectsCursor)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second"))};
|
||||
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, 5);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Second")); // 5 % 2 == 1
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, RandomStaysWithinList)
|
||||
{
|
||||
DeckList deck;
|
||||
const QList<PlaymatInfo> fallback = {makePlaymatInfo(QStringLiteral("First")),
|
||||
makePlaymatInfo(QStringLiteral("Second")),
|
||||
makePlaymatInfo(QStringLiteral("Third"))};
|
||||
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRandom, i);
|
||||
ASSERT_FALSE(resolved.card.name.isEmpty());
|
||||
EXPECT_TRUE(fallback.contains(resolved));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PlaymatResolverTest, ForceWithEmptyCardIgnoresFallbackParamsButNotFallback)
|
||||
{
|
||||
DeckList deck;
|
||||
deck.setPlaymat({{QStringLiteral("Deck Mat")}, {}});
|
||||
|
||||
// An empty force entry must not mask the deck-configured playmat.
|
||||
const PlaymatInfo emptyForce;
|
||||
const PlaymatInfo resolved = resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackModeFixed, 0);
|
||||
EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat"));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
137
tests/server_developer_role_test.cpp
Normal file
137
tests/server_developer_role_test.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/** @file server_developer_role_test.cpp
|
||||
* @brief Tests for the developer staff role authorization and dispatch.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/server/remote/server.h>
|
||||
#include <libcockatrice/network/server/remote/server_protocolhandler.h>
|
||||
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/developer_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
|
||||
// The server_remote library references the global RNG, which is normally
|
||||
// defined by the servatrice/client executable main(). Provide a stub so the
|
||||
// unit test can link against it.
|
||||
RNG_Abstract *rng = nullptr;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class TestDeveloperHandler : public Server_ProtocolHandler
|
||||
{
|
||||
public:
|
||||
explicit TestDeveloperHandler(Server *_server) : Server_ProtocolHandler(_server, nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
QString getAddress() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
QString getConnectionType() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// Buffer the last response code sent to the client so tests can assert on
|
||||
// the outcome of processCommandContainer().
|
||||
Response::ResponseCode lastResponseCode = Response::RespNothing;
|
||||
int dispatchCount = 0;
|
||||
|
||||
protected:
|
||||
void transmitProtocolItem(const ServerMessage &item) override
|
||||
{
|
||||
if (item.message_type() == ServerMessage::RESPONSE) {
|
||||
lastResponseCode = item.response().response_code();
|
||||
}
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
processExtendedDeveloperCommand(int cmdType, const DeveloperCommand &, ResponseContainer &) override
|
||||
{
|
||||
++dispatchCount;
|
||||
// Fail closed for anything not explicitly handled.
|
||||
if (cmdType != DeveloperCommand::GET_SERVER_STATS) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
return Response::RespOk;
|
||||
}
|
||||
};
|
||||
|
||||
class DeveloperRoleTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Server server;
|
||||
TestDeveloperHandler handler{&server};
|
||||
|
||||
void setUserLevel(uint32_t level)
|
||||
{
|
||||
ServerInfo_User user;
|
||||
user.set_user_level(level);
|
||||
handler.setUserInfo(user);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DeveloperRoleTest, RejectsWhenNotLoggedIn)
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.add_developer_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
EXPECT_EQ(handler.dispatchCount, 0);
|
||||
}
|
||||
|
||||
TEST_F(DeveloperRoleTest, RejectsPlainUser)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_developer_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
EXPECT_EQ(handler.dispatchCount, 0);
|
||||
}
|
||||
|
||||
TEST_F(DeveloperRoleTest, RejectsModeratorThatIsNotDeveloper)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsModerator);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_developer_command();
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||
}
|
||||
|
||||
TEST_F(DeveloperRoleTest, DispatchesToDeveloperCommandForDeveloper)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsDeveloper);
|
||||
|
||||
CommandContainer cont;
|
||||
DeveloperCommand *cmd = cont.add_developer_command();
|
||||
cmd->MutableExtension(Command_GetServerStats::ext);
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
|
||||
EXPECT_EQ(handler.dispatchCount, 1);
|
||||
}
|
||||
|
||||
TEST_F(DeveloperRoleTest, FailClosedForUnknownDeveloperCommand)
|
||||
{
|
||||
setUserLevel(ServerInfo_User::IsDeveloper);
|
||||
|
||||
CommandContainer cont;
|
||||
cont.add_developer_command(); // no extension set -> getPbExtension() returns -1
|
||||
handler.processCommandContainer(cont);
|
||||
EXPECT_EQ(handler.lastResponseCode, Response::RespFunctionNotAllowed);
|
||||
EXPECT_EQ(handler.dispatchCount, 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
174
tests/server_game_join_test.cpp
Normal file
174
tests/server_game_join_test.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/** @file server_game_join_test.cpp
|
||||
* @brief Tests for the moderator/judge game-entry restriction override in Server_Game::checkJoin.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "game/server_game.h"
|
||||
#include "server.h"
|
||||
#include "server_database_interface.h"
|
||||
#include "server_room.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
|
||||
RNG_Abstract *rng = nullptr; // referenced by the server_remote library
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class MockDatabaseInterface : public Server_DatabaseInterface
|
||||
{
|
||||
public:
|
||||
AuthenticationResult checkUserPassword(Server_ProtocolHandler *,
|
||||
const QString &,
|
||||
const QString &,
|
||||
const QString &,
|
||||
QString &,
|
||||
int &,
|
||||
bool) override
|
||||
{
|
||||
return NotLoggedIn;
|
||||
}
|
||||
int getNextReplayId() override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
int getNextGameId() override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
int getActiveUserCount(QString) override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
ServerInfo_User getUserData(const QString &, bool) override
|
||||
{
|
||||
return ServerInfo_User();
|
||||
}
|
||||
};
|
||||
|
||||
class FakeServer : public Server
|
||||
{
|
||||
public:
|
||||
FakeServer()
|
||||
{
|
||||
setDatabaseInterface(new MockDatabaseInterface());
|
||||
}
|
||||
};
|
||||
|
||||
class GameJoinOverrideTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
FakeServer server;
|
||||
Server_Room room{0, 0, "", "", "", "", false, "", {}, &server};
|
||||
ServerInfo_User creator;
|
||||
ServerInfo_User plainUser;
|
||||
ServerInfo_User unregisteredJudge;
|
||||
ServerInfo_User moderator;
|
||||
ServerInfo_User judge;
|
||||
Server_Game *game = nullptr;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
creator.set_name("creator");
|
||||
creator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||
plainUser.set_name("plain-user");
|
||||
plainUser.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||
unregisteredJudge.set_name("unregistered-judge");
|
||||
unregisteredJudge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsJudge);
|
||||
moderator.set_name("moderator");
|
||||
moderator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered |
|
||||
ServerInfo_User::IsModerator);
|
||||
judge.set_name("judge");
|
||||
judge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsJudge);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete game;
|
||||
}
|
||||
|
||||
Server_Game *makeGame(bool passwordProtected, bool onlyRegistered, bool onlyBuddies, bool spectatorsAllowed)
|
||||
{
|
||||
GameConfig config{.creatorInfo = creator,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = passwordProtected ? "secret" : QString(),
|
||||
.maxPlayers = 2,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = onlyBuddies,
|
||||
.onlyRegistered = onlyRegistered,
|
||||
.spectatorsAllowed = spectatorsAllowed,
|
||||
.spectatorsNeedPassword = true,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false};
|
||||
return new Server_Game(config, &room);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction)
|
||||
{
|
||||
game = makeGame(true, false, false, true);
|
||||
|
||||
// A plain user cannot override the password even with the override flag set.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword);
|
||||
// Moderators and judges may enter any game regardless of the password.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk);
|
||||
// Without the override flag judges are still subject to the password.
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
|
||||
{
|
||||
game = makeGame(false, true, false, true);
|
||||
|
||||
// Without the override flag the only-registered restriction still applies.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow);
|
||||
// An unregistered judge may enter when overriding restrictions.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
|
||||
{
|
||||
game = makeGame(false, false, true, true);
|
||||
|
||||
// A plain user who is not on the creator's buddy list gets rejected.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies);
|
||||
// Moderators and judges bypass the buddies-only restriction.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
|
||||
{
|
||||
game = makeGame(false, false, false, false);
|
||||
|
||||
// A plain user cannot spectate when the game disallows spectators.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed);
|
||||
// Moderators and judges may spectate any game regardless of the password
|
||||
// and the spectator restriction.
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
|
||||
{
|
||||
game = makeGame(false, false, false, true);
|
||||
|
||||
// joining with join_as_judge still requires the judge flag even when overriding.
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
151
tests/server_rate_limiter_test.cpp
Normal file
151
tests/server_rate_limiter_test.cpp
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QDateTime>
|
||||
#include <QTimeZone>
|
||||
#include <libcockatrice/utility/server_rate_limiter.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const QString HOST = "api.scryfall.com";
|
||||
|
||||
QDateTime timeAt(qint64 secsFromEpoch)
|
||||
{
|
||||
return QDateTime::fromMSecsSinceEpoch(secsFromEpoch * 1000, QTimeZone::UTC);
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, First429SetsThirtySecondBackoff)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
QDateTime deadline = limiter.on429(HOST, now);
|
||||
|
||||
EXPECT_TRUE(limiter.isRateLimited(HOST, now));
|
||||
EXPECT_EQ(now.addSecs(30), deadline);
|
||||
EXPECT_EQ(1, limiter.rounds(HOST));
|
||||
EXPECT_FALSE(limiter.isRateLimited(HOST, deadline));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, RetryAfterHeaderIsHonoredWhenLarger)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
QDateTime deadline = limiter.on429(HOST, now, 45 * 1000);
|
||||
|
||||
EXPECT_EQ(now.addSecs(45), deadline);
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, RetryAfterSmallerThanFloorIsIgnored)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
QDateTime deadline = limiter.on429(HOST, now, 5 * 1000);
|
||||
|
||||
EXPECT_EQ(now.addSecs(30), deadline);
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, Concurrent429sDoNotEscalateOrShorten)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
QDateTime first = limiter.on429(HOST, now);
|
||||
limiter.on429(HOST, now.addMSecs(100));
|
||||
limiter.on429(HOST, now.addMSecs(200));
|
||||
|
||||
// The burst counts as a single round, but each 429 slides the deadline forward.
|
||||
EXPECT_EQ(1, limiter.rounds(HOST));
|
||||
EXPECT_EQ(now.addMSecs(30200), limiter.deadline(HOST));
|
||||
EXPECT_TRUE(limiter.deadline(HOST) >= first);
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, EscalatesAfterWindowPasses)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
limiter.on429(HOST, now);
|
||||
limiter.on429(HOST, now.addSecs(31));
|
||||
|
||||
EXPECT_EQ(2, limiter.rounds(HOST));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, BudgetExhaustionAppliesCooldown)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
QDateTime deadline;
|
||||
for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) {
|
||||
deadline = limiter.on429(HOST, now.addSecs(31 * i));
|
||||
}
|
||||
|
||||
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
||||
EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 60), deadline);
|
||||
|
||||
// A 429 inside the exhausted cooldown does not escalate, but keeps sliding the cooldown forward.
|
||||
limiter.on429(HOST, now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30));
|
||||
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
||||
EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30 + 60), limiter.deadline(HOST));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, SuccessClearsPenalty)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
limiter.on429(HOST, now);
|
||||
limiter.onSuccess(HOST);
|
||||
|
||||
EXPECT_EQ(0, limiter.rounds(HOST));
|
||||
EXPECT_FALSE(limiter.isRateLimited(HOST, now));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, ClearExpiredRefreshesStaleBudget)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
limiter.on429(HOST, now);
|
||||
limiter.clearExpired(now.addSecs(400));
|
||||
|
||||
EXPECT_EQ(0, limiter.rounds(HOST));
|
||||
EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(400)));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, ClearExpiredKeepsRecentBudget)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) {
|
||||
limiter.on429(HOST, now.addSecs(31 * i));
|
||||
}
|
||||
limiter.clearExpired(now.addSecs(200));
|
||||
|
||||
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
||||
EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(200)));
|
||||
}
|
||||
|
||||
TEST(ServerRateLimiterTest, EarliestDeadlineAcrossHosts)
|
||||
{
|
||||
ServerRateLimiter limiter;
|
||||
QDateTime now = timeAt(1000);
|
||||
|
||||
limiter.on429("api.scryfall.com", now, 40 * 1000);
|
||||
limiter.on429("gatherer.wizards.com", now.addSecs(5));
|
||||
|
||||
EXPECT_EQ(now.addSecs(35), limiter.earliestDeadline(now));
|
||||
EXPECT_EQ(now.addSecs(40), limiter.deadline("api.scryfall.com"));
|
||||
EXPECT_EQ(now.addSecs(40), limiter.earliestDeadline(now.addSecs(36)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
26
tests/settings/CMakeLists.txt
Normal file
26
tests/settings/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
add_executable(settings_manager_test settings_manager_test.cpp)
|
||||
add_executable(settings_defaults_test settings_defaults_test.cpp)
|
||||
add_executable(settings_migration_test settings_migration_test.cpp)
|
||||
|
||||
target_link_libraries(
|
||||
settings_manager_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
settings_defaults_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
target_compile_definitions(settings_defaults_test PRIVATE SETTINGS_UNIT_TEST)
|
||||
|
||||
target_link_libraries(
|
||||
settings_migration_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME settings_manager_test COMMAND settings_manager_test)
|
||||
add_test(NAME settings_defaults_test COMMAND settings_defaults_test)
|
||||
add_test(NAME settings_migration_test COMMAND settings_migration_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(settings_manager_test gtest)
|
||||
add_dependencies(settings_defaults_test gtest)
|
||||
add_dependencies(settings_migration_test gtest)
|
||||
endif()
|
||||
612
tests/settings/settings_defaults_test.cpp
Normal file
612
tests/settings/settings_defaults_test.cpp
Normal file
|
|
@ -0,0 +1,612 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/game_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class SettingsDefaultsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir tempDir;
|
||||
QString settingsPath;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
settingsPath = tempDir.path() + "/";
|
||||
}
|
||||
};
|
||||
|
||||
// --- CacheStorageSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_PixmapCacheSize_Default)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getPixmapCacheSize(), PIXMAPCACHE_SIZE_DEFAULT);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_PixmapCacheSize_SetAndGet)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
s.setPixmapCacheSize(1024);
|
||||
ASSERT_EQ(s.getPixmapCacheSize(), 1024);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_NetworkCacheSizeInMB_Default)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getNetworkCacheSizeInMB(), NETWORK_CACHE_SIZE_DEFAULT);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_RedirectCacheTtl_Default)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getRedirectCacheTtl(), NETWORK_REDIRECT_CACHE_TTL_DEFAULT);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_CardPictureLoaderCacheMethod_Default)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCardPictureLoaderCacheMethod(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CacheStorage_LocalCardImageStorageNamingScheme_Default)
|
||||
{
|
||||
CacheStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getLocalCardImageStorageNamingScheme(), LOCAL_CARD_IMAGE_NAMING_SCHEME_DEFAULT);
|
||||
}
|
||||
|
||||
// --- GameSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_MaxPlayers_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getMaxPlayers(), 2);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_MaxPlayers_SetAndGet)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
s.setMaxPlayers(4);
|
||||
ASSERT_EQ(s.getMaxPlayers(), 4);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_DefaultStartingLifeTotal_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getDefaultStartingLifeTotal(), 20);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_RememberGameSettings_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getRememberGameSettings(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_LocalGameMaxPlayers_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getLocalGameMaxPlayers(), 1);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_LocalGameStartingLifeTotal_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getLocalGameStartingLifeTotal(), 20);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Game_LocalGameRememberSettings_Default)
|
||||
{
|
||||
GameSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getLocalGameRememberSettings(), false);
|
||||
}
|
||||
|
||||
// --- UpdatesSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_CheckUpdatesOnStartup_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCheckUpdatesOnStartup(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_StartupCardUpdateCheckPromptForUpdate_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getStartupCardUpdateCheckPromptForUpdate(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_StartupCardUpdateCheckAlwaysUpdate_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getStartupCardUpdateCheckAlwaysUpdate(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_CardUpdateCheckInterval_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCardUpdateCheckInterval(), 7);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_AlwaysEnableNewSets_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getAlwaysEnableNewSets(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_NotifyAboutUpdates_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getNotifyAboutUpdates(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_NotifyAboutNewVersion_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getNotifyAboutNewVersion(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Updates_UpdateReleaseChannelIndex_Default)
|
||||
{
|
||||
UpdatesSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getUpdateReleaseChannelIndex(), 0);
|
||||
}
|
||||
|
||||
// --- SoundSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Sound_SoundEnabled_Default)
|
||||
{
|
||||
SoundSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getSoundEnabled(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Sound_MasterVolume_Default)
|
||||
{
|
||||
SoundSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getMasterVolume(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Sound_MasterVolume_SetAndGet)
|
||||
{
|
||||
SoundSettings s(settingsPath, nullptr);
|
||||
s.setMasterVolume(50);
|
||||
ASSERT_EQ(s.getMasterVolume(), 50);
|
||||
}
|
||||
|
||||
// --- TabsSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(StartupTab::StartupTabHome));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_SetAndGet)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
s.setStartupTabIndex(StartupTab::StartupTabServerRoom);
|
||||
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(StartupTab::StartupTabServerRoom));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_StartupServer_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getStartupServerHost(), QString());
|
||||
ASSERT_EQ(s.getStartupServerPort(), QString());
|
||||
ASSERT_EQ(s.getStartupRoomName(), QString());
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_StartupServer_SetAndGet)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
s.setStartupServerHost("server.cockatrice.us");
|
||||
s.setStartupServerPort("4748");
|
||||
s.setStartupRoomName("General");
|
||||
ASSERT_EQ(s.getStartupServerHost(), QString("server.cockatrice.us"));
|
||||
ASSERT_EQ(s.getStartupServerPort(), QString("4748"));
|
||||
ASSERT_EQ(s.getStartupRoomName(), QString("General"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTabVisualDeckStorageOpen(), true);
|
||||
ASSERT_EQ(s.getTabServerOpen(), true);
|
||||
ASSERT_EQ(s.getTabAccountOpen(), true);
|
||||
ASSERT_EQ(s.getTabDeckStorageOpen(), true);
|
||||
ASSERT_EQ(s.getTabReplaysOpen(), true);
|
||||
ASSERT_EQ(s.getTabAdminOpen(), true);
|
||||
ASSERT_EQ(s.getTabLogOpen(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTabModerationOpen(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Tabs_CardArtRulesOpen_Default)
|
||||
{
|
||||
TabsSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTabCardArtRulesOpen(), false);
|
||||
}
|
||||
|
||||
// --- ChatSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_Mention_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatMention(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_MentionCompleter_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatMentionCompleter(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_MentionColor_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatMentionColor(), QString("A6120D"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_HighlightColor_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatHighlightColor(), QString("A6120D"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_MentionForeground_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatMentionForeground(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_HighlightForeground_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getChatHighlightForeground(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_ShowMessagePopup_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowMessagePopup(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_ShowMentionPopup_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowMentionPopup(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_RoomHistory_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getRoomHistory(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Chat_IgnoreAllPrivateMessages_Default)
|
||||
{
|
||||
ChatSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getIgnoreAllPrivateMessages(), false);
|
||||
}
|
||||
|
||||
// --- PersonalSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Personal_Lang_Default)
|
||||
{
|
||||
PersonalSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getLang(), QString(""));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Personal_ShowTipsOnStartup_Default)
|
||||
{
|
||||
PersonalSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowTipsOnStartup(), true);
|
||||
}
|
||||
|
||||
// --- DownloadSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Download_PicDownload_Default)
|
||||
{
|
||||
DownloadSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getPicDownload(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Download_DownloadSpoilersStatus_Default)
|
||||
{
|
||||
DownloadSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getDownloadSpoilersStatus(), false);
|
||||
}
|
||||
|
||||
// --- AppearanceSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_ThemeName_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getThemeName(), QString(""));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_ThemeName_SetAndGet)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
s.setThemeName("my_theme");
|
||||
ASSERT_EQ(s.getThemeName(), QString("my_theme"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getStyleUserList(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getMaxFontSize(), 12);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_SetAndGet)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
s.setMaxFontSize(14);
|
||||
ASSERT_EQ(s.getMaxFontSize(), 14);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getHomeTabBackgroundSource(), QString("themed"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundShuffleFrequency_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getHomeTabBackgroundShuffleFrequency(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getHomeTabDisplayCardName(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_Default)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getHomeTabBackgroundDim(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_SetAndGet)
|
||||
{
|
||||
AppearanceSettings s(settingsPath, nullptr);
|
||||
s.setHomeTabBackgroundDim(false);
|
||||
ASSERT_EQ(s.getHomeTabBackgroundDim(), false);
|
||||
}
|
||||
|
||||
// --- InterfaceSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowStatusBar(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_ShowShortcuts_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowShortcuts(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_ShowGameSelectorFilterToolbar_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getShowGameSelectorFilterToolbar(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_NotificationsEnabled_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getNotificationsEnabled(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_SpectatorNotificationsEnabled_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getSpectatorNotificationsEnabled(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Interface_BuddyConnectNotificationsEnabled_Default)
|
||||
{
|
||||
InterfaceSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getBuddyConnectNotificationsEnabled(), true);
|
||||
}
|
||||
|
||||
// --- DeckEditorSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_OpenDeckInNewTab_Default)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getOpenDeckInNewTab(), false);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_BannerCardComboBoxVisible_Default)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getBannerCardComboBoxVisible(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_TagsWidgetVisible_Default)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTagsWidgetVisible(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_DefaultDeckEditorType_Default)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getDefaultDeckEditorType(), 1);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_Default)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_SetAndGet)
|
||||
{
|
||||
DeckEditorSettings s(settingsPath, nullptr);
|
||||
s.setVdeStartupTab(VdeStartupTabDeckDisplay);
|
||||
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDeckDisplay);
|
||||
s.setVdeStartupTab(VdeStartupTabDatabaseDisplay);
|
||||
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDatabaseDisplay);
|
||||
s.setVdeStartupTab(VdeStartupTabContext);
|
||||
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext);
|
||||
}
|
||||
|
||||
// --- NetworkSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)
|
||||
{
|
||||
NetworkSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getClientID(), QString("notset"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Network_ClientVersion_Default)
|
||||
{
|
||||
NetworkSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getClientVersion(), QString("notset"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Network_KeepAlive_Default)
|
||||
{
|
||||
NetworkSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getKeepAlive(), 3);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Network_TimeOut_Default)
|
||||
{
|
||||
NetworkSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getTimeOut(), 5);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Network_KnownMissingFeatures_Default)
|
||||
{
|
||||
NetworkSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getKnownMissingFeatures(), QString(""));
|
||||
}
|
||||
|
||||
// --- CardsDisplaySettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_PrintingSelectorCardSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getPrintingSelectorCardSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckStorageCardSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDeckStorageCardSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDatabaseDisplayCardSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDatabaseDisplayCardSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckEditorCardSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDeckEditorCardSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_EDHRecCardSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getEDHRecCardSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_ArchidektPreviewSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getArchidektPreviewSize(), 100);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getSampleHandSize(), 7);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getArrowDrawAnimation(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCardLang(), QString("en"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
s.setCardLang("de");
|
||||
ASSERT_EQ(s.getCardLang(), QString("de"));
|
||||
}
|
||||
|
||||
// --- VisualDeckStorageSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDeckStorageSortingOrder(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowFolders_Default)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDeckStorageShowFolders(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_DefaultTagsList_NotEmpty)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_GT(s.getVisualDeckStorageDefaultTagsList().size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_DefaultTagsList_SetAndGet)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
QStringList custom = {"Tag1", "Tag2"};
|
||||
s.setVisualDeckStorageDefaultTagsList(custom);
|
||||
ASSERT_EQ(s.getVisualDeckStorageDefaultTagsList(), custom);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
171
tests/settings/settings_manager_test.cpp
Normal file
171
tests/settings/settings_manager_test.cpp
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/settings/settings_manager.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class SettingsManagerTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir tempDir;
|
||||
QString settingsPath;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
settingsPath = tempDir.path() + "/";
|
||||
}
|
||||
|
||||
void writeValue(const QString &file, const QString &key, const QVariant &value)
|
||||
{
|
||||
QSettings qs(file, QSettings::IniFormat);
|
||||
qs.setValue(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SettingsManagerTest, GetValueNoDefaultReturnsInvalid)
|
||||
{
|
||||
SettingsManager sm(settingsPath + "test.ini");
|
||||
QVariant val = sm.getValue("nonexistent");
|
||||
ASSERT_TRUE(val.isNull());
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, GetValueWithDefaultReturnsDefaultWhenMissing)
|
||||
{
|
||||
SettingsManager sm(settingsPath + "test.ini");
|
||||
QVariant val = sm.getValue("nonexistent", QString(), QString(), 42);
|
||||
ASSERT_EQ(val.toInt(), 42);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, GetValueWithDefaultReturnsStoredWhenPresent)
|
||||
{
|
||||
writeValue(settingsPath + "test.ini", "mykey", 99);
|
||||
|
||||
SettingsManager sm(settingsPath + "test.ini");
|
||||
QVariant val = sm.getValue("mykey", QString(), QString(), 42);
|
||||
ASSERT_EQ(val.toInt(), 99);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, GetValueWithGroup)
|
||||
{
|
||||
writeValue(settingsPath + "grouped.ini", "group1/greeting", "world");
|
||||
|
||||
SettingsManager sm(settingsPath + "grouped.ini");
|
||||
QVariant val = sm.getValue("greeting", "group1");
|
||||
ASSERT_EQ(val.toString(), QString("world"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, GetValueWithGroupAndSubGroup)
|
||||
{
|
||||
writeValue(settingsPath + "nested.ini", "outer/inner/depth", 1);
|
||||
|
||||
SettingsManager sm(settingsPath + "nested.ini");
|
||||
QVariant val = sm.getValue("depth", "outer", "inner");
|
||||
ASSERT_EQ(val.toInt(), 1);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, DefaultGroupUsedInGetValue)
|
||||
{
|
||||
writeValue(settingsPath + "defgroup.ini", "defaultGroup/key", "val");
|
||||
|
||||
SettingsManager sm(settingsPath + "defgroup.ini", "defaultGroup");
|
||||
QVariant val = sm.getValue("key");
|
||||
ASSERT_EQ(val.toString(), QString("val"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, DefaultGroupScopesGetValue)
|
||||
{
|
||||
writeValue(settingsPath + "scoped.ini", "key", "ungrouped");
|
||||
writeValue(settingsPath + "scoped.ini", "defaultGroup/key", "grouped");
|
||||
|
||||
SettingsManager sm(settingsPath + "scoped.ini", "defaultGroup");
|
||||
QVariant val = sm.getValue("key");
|
||||
ASSERT_EQ(val.toString(), QString("grouped"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, DefaultSubGroupUsedInGetValue)
|
||||
{
|
||||
writeValue(settingsPath + "sub.ini", "top/bottom/key", "nested");
|
||||
|
||||
SettingsManager sm(settingsPath + "sub.ini", "top", "bottom");
|
||||
QVariant val = sm.getValue("key");
|
||||
ASSERT_EQ(val.toString(), QString("nested"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, BatchWriteWritesAllValues)
|
||||
{
|
||||
SettingsManager sm(settingsPath + "batch.ini");
|
||||
sm.batchWrite([&](QSettings &qs) {
|
||||
qs.setValue("a", 1);
|
||||
qs.setValue("b", 2);
|
||||
qs.setValue("c", 3);
|
||||
});
|
||||
|
||||
SettingsManager sm2(settingsPath + "batch.ini");
|
||||
ASSERT_EQ(sm2.getValue("a", QString(), QString(), 0).toInt(), 1);
|
||||
ASSERT_EQ(sm2.getValue("b", QString(), QString(), 0).toInt(), 2);
|
||||
ASSERT_EQ(sm2.getValue("c", QString(), QString(), 0).toInt(), 3);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, BatchWriteWithGroup)
|
||||
{
|
||||
SettingsManager sm(settingsPath + "batch_group.ini");
|
||||
sm.batchWrite([&](QSettings &qs) {
|
||||
qs.beginGroup("section");
|
||||
qs.setValue("x", 10);
|
||||
qs.endGroup();
|
||||
});
|
||||
|
||||
SettingsManager sm2(settingsPath + "batch_group.ini");
|
||||
ASSERT_EQ(sm2.getValue("x", "section", QString(), 0).toInt(), 10);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, SyncPersistsToDisk)
|
||||
{
|
||||
QString filePath = settingsPath + "sync_test.ini";
|
||||
{
|
||||
SettingsManager sm(filePath);
|
||||
sm.batchWrite([&](QSettings &qs) { qs.setValue("persist", "yes"); });
|
||||
}
|
||||
{
|
||||
QSettings qs(filePath, QSettings::IniFormat);
|
||||
ASSERT_EQ(qs.value("persist").toString(), QString("yes"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, MultipleReadsAfterWrite)
|
||||
{
|
||||
SettingsManager sm(settingsPath + "multi.ini");
|
||||
sm.batchWrite([&](QSettings &qs) { qs.setValue("k1", 1); });
|
||||
|
||||
QVariant v1 = sm.getValue("k1", QString(), QString(), 0);
|
||||
QVariant v2 = sm.getValue("nonexistent", QString(), QString(), 99);
|
||||
|
||||
ASSERT_EQ(v1.toInt(), 1);
|
||||
ASSERT_EQ(v2.toInt(), 99);
|
||||
}
|
||||
|
||||
TEST_F(SettingsManagerTest, ValuesDeletedViaQSettingsAreReadAsDefault)
|
||||
{
|
||||
writeValue(settingsPath + "del.ini", "deleteMe", "present");
|
||||
|
||||
SettingsManager sm(settingsPath + "del.ini");
|
||||
ASSERT_EQ(sm.getValue("deleteMe", QString(), QString(), "default").toString(), QString("present"));
|
||||
|
||||
// Delete via QSettings directly (deleteValue is protected)
|
||||
{
|
||||
QSettings qs(settingsPath + "del.ini", QSettings::IniFormat);
|
||||
qs.remove("deleteMe");
|
||||
}
|
||||
|
||||
ASSERT_EQ(sm.getValue("deleteMe", QString(), QString(), "default").toString(), QString("default"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
710
tests/settings/settings_migration_test.cpp
Normal file
710
tests/settings/settings_migration_test.cpp
Normal file
|
|
@ -0,0 +1,710 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/settings/settings_migration.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class SettingsMigrationTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir tempDir;
|
||||
QString settingsPath;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
settingsPath = tempDir.path() + "/";
|
||||
|
||||
// Isolate the settings used by the legacy migration tests inside the temporary
|
||||
// directory so the tests never read or write the real user config (registry on
|
||||
// Windows, plist on macOS, .conf on Linux), which would otherwise be shared
|
||||
// across CI jobs and flaky. Default-format QSettings is forced to IniFormat and
|
||||
// its UserScope path is redirected here; setPath wins over the XDG_CONFIG_HOME
|
||||
// default on Unix, so every platform resolves to <tempDir>/config/...
|
||||
const QString configDir = tempDir.path() + "/config";
|
||||
QDir().mkpath(configDir);
|
||||
qputenv("XDG_CONFIG_HOME", configDir.toUtf8());
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, configDir);
|
||||
}
|
||||
|
||||
bool fileExists(const QString &name) const
|
||||
{
|
||||
return QFile::exists(settingsPath + name);
|
||||
}
|
||||
|
||||
QVariant readFromIni(const QString &fileName, const QString &key) const
|
||||
{
|
||||
QSettings ini(settingsPath + fileName, QSettings::IniFormat);
|
||||
return ini.value(key);
|
||||
}
|
||||
|
||||
// Checks for the exact legacy key in the raw INI content. On Windows, INI keys are
|
||||
// case-insensitive, so reading back a lower-case legacy key would match its migrated
|
||||
// camelCase counterpart and hide regressions. Checking the file bytes directly keeps
|
||||
// the comparison case-sensitive on every platform.
|
||||
bool iniFileHasKeyCaseSensitive(const QString &fileName, const QString &key) const
|
||||
{
|
||||
QFile f(settingsPath + fileName);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
const QString valueName = key.section('/', -1);
|
||||
const QStringList lines = QString::fromUtf8(f.readAll()).split('\n');
|
||||
for (const auto &line : lines) {
|
||||
if (line.startsWith(valueName + '=')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SettingsMigrationTest, NoGlobalIniDoesNothing)
|
||||
{
|
||||
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
|
||||
ASSERT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, EmptyGlobalIniGetsBackedUp)
|
||||
{
|
||||
{
|
||||
QFile f(settingsPath + "global.ini");
|
||||
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
|
||||
f.close();
|
||||
}
|
||||
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
ASSERT_TRUE(fileExists("global.ini"));
|
||||
|
||||
// No per-file INIs should be created
|
||||
ASSERT_FALSE(fileExists("tabs.ini"));
|
||||
ASSERT_FALSE(fileExists("sound.ini"));
|
||||
|
||||
// Sentinel should be in the new global.ini
|
||||
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
|
||||
// tabs
|
||||
g.setValue("tabs/visualDeckStorage", false);
|
||||
g.setValue("tabs/server", true);
|
||||
g.setValue("tabs/account", true);
|
||||
g.setValue("tabs/deckStorage", true);
|
||||
g.setValue("tabs/replays", false);
|
||||
g.setValue("tabs/admin", false);
|
||||
g.setValue("tabs/log", true);
|
||||
|
||||
// sound
|
||||
g.setValue("sound/enabled", true);
|
||||
g.setValue("sound/theme", "custom_theme");
|
||||
g.setValue("sound/mastervolume", 75);
|
||||
|
||||
// game
|
||||
g.beginGroup("game");
|
||||
g.setValue("maxplayers", 4);
|
||||
g.setValue("gamedescription", "test game");
|
||||
g.setValue("remembergamesettings", false);
|
||||
g.setValue("gametypes", "commander");
|
||||
g.setValue("onlybuddies", true);
|
||||
g.endGroup();
|
||||
|
||||
// localgameoptions
|
||||
g.beginGroup("localgameoptions");
|
||||
g.setValue("maxplayers", 2);
|
||||
g.setValue("startinglifetotal", 40);
|
||||
g.endGroup();
|
||||
|
||||
// chat
|
||||
g.beginGroup("chat");
|
||||
g.setValue("mention", false);
|
||||
g.setValue("mentioncolor", "FF0000");
|
||||
g.setValue("showmessagepopups", false);
|
||||
g.setValue("mentioncompleter", false);
|
||||
g.setValue("roomhistory", false);
|
||||
g.setValue("highlightcolor", "00FF00");
|
||||
g.endGroup();
|
||||
|
||||
// legacy highlight words (under [personal])
|
||||
g.setValue("personal/highlightWords", "alpha beta");
|
||||
|
||||
// cache storage (under [personal] group)
|
||||
g.setValue("personal/pixmapCacheSize", 1024);
|
||||
g.setValue("personal/networkCacheSize", 2048);
|
||||
g.setValue("personal/redirectCacheTtl", 5);
|
||||
g.setValue("personal/cardPictureLoaderCacheMethod", 1);
|
||||
g.setValue("personal/localCardImageStorageNamingScheme", 2);
|
||||
|
||||
// updates (under [personal] group)
|
||||
g.setValue("personal/startupUpdateCheck", false);
|
||||
g.setValue("personal/startupCardUpdateCheckPromptForUpdate", false);
|
||||
g.setValue("personal/startupCardUpdateCheckAlwaysUpdate", true);
|
||||
g.setValue("personal/cardUpdateCheckInterval", 14);
|
||||
g.setValue("personal/lastCardUpdateCheck", QDate(2024, 1, 1));
|
||||
g.setValue("personal/alwaysEnableNewSets", true);
|
||||
g.setValue("personal/updatenotification", false);
|
||||
g.setValue("personal/newversionnotification", false);
|
||||
|
||||
// personal
|
||||
g.setValue("personal/lang", "de");
|
||||
|
||||
// downloads (previously under [personal])
|
||||
g.setValue("personal/picturedownload", true);
|
||||
g.setValue("personal/downloadspoilers", true);
|
||||
|
||||
// interface (previously under [personal])
|
||||
g.setValue("personal/showStatusBar", true);
|
||||
|
||||
// theme
|
||||
g.setValue("theme/name", "custom_theme");
|
||||
g.setValue("game/maxfontsize", 14);
|
||||
|
||||
// appearance
|
||||
g.setValue("appearance/styleUserList", false);
|
||||
g.setValue("home/background/displayCardName", false);
|
||||
g.setValue("menu/showshortcuts", false);
|
||||
g.setValue("menu/showgameselectorfiltertoolbar", false);
|
||||
|
||||
// deck editor
|
||||
g.setValue("editor/openDeckInNewTab", false);
|
||||
g.setValue("interface/deckeditortagswidgetvisible", false);
|
||||
g.setValue("interface/defaultDeckEditorType", 0);
|
||||
g.setValue("interface/visualdeckeditorsamplehandsize", 5);
|
||||
|
||||
// personal home
|
||||
g.setValue("home/background", "custom_bg");
|
||||
g.setValue("home/background/shuffleTimer", 30);
|
||||
|
||||
// personal tipOfDay
|
||||
g.setValue("tipOfDay/showTips", false);
|
||||
g.setValue("tipOfDay/seenTips", QStringList{"1", "2", "3"});
|
||||
|
||||
// network
|
||||
g.setValue("personal/keepalive", 10);
|
||||
g.setValue("personal/timeout", 30);
|
||||
g.setValue("personal/clientid", "test-client-id");
|
||||
g.setValue("personal/clientversion", "test-client-version");
|
||||
g.setValue("interface/knownmissingfeatures", "feature1,feature2");
|
||||
|
||||
// cards
|
||||
g.setValue("cards/displaycardnames", false);
|
||||
g.setValue("cards/roundcardcorners", false);
|
||||
g.setValue("cards/overrideallcardartwithpersonalpreference", true);
|
||||
g.setValue("cards/bumpsetswithcardsindecktotop", false);
|
||||
g.setValue("cards/includerebalancedcards", false);
|
||||
g.setValue("cards/autorotatesidewayslayoutcards", false);
|
||||
g.setValue("cards/tapanimation", true);
|
||||
g.setValue("cards/scaleCards", false);
|
||||
g.setValue("cards/verticalCardOverlapPercent", 42);
|
||||
g.setValue("cards/cardinfoviewmode", 1);
|
||||
g.setValue("cards/printingselectorcardsize", 90);
|
||||
g.setValue("cards/printingselectorsortorder", 3);
|
||||
g.setValue("cards/printingselectornavigationbuttonsvisible", false);
|
||||
// card counters (migrate into card_counters.ini)
|
||||
g.setValue("cards/counters/0/color", QColor(Qt::red));
|
||||
|
||||
// interface
|
||||
g.setValue("interface/usetearoffmenus", true);
|
||||
g.setValue("interface/cardViewInitialRowsMax", 8);
|
||||
g.setValue("interface/cardViewExpandedRowsMax", 12);
|
||||
g.setValue("interface/closeEmptyCardView", false);
|
||||
g.setValue("interface/focusCardViewSearchBar", false);
|
||||
g.setValue("interface/keepGameChatFocus", true);
|
||||
g.setValue("interface/notificationsenabled", false);
|
||||
g.setValue("interface/specnotificationsenabled", true);
|
||||
g.setValue("interface/buddyconnectnotificationsenabled", false);
|
||||
g.setValue("interface/doubleclicktoplay", false);
|
||||
g.setValue("interface/clickPlaysAllSelected", false);
|
||||
g.setValue("interface/playtostack", false);
|
||||
g.setValue("interface/doNotDeleteArrowsInSubPhases", false);
|
||||
g.setValue("interface/startinghandsize", 5);
|
||||
g.setValue("interface/annotatetokens", true);
|
||||
g.setValue("interface/showlassoselectioncount", false);
|
||||
g.setValue("interface/showpersistentselectioncount", false);
|
||||
g.setValue("interface/tallyType", 2);
|
||||
g.setValue("interface/leftjustified", true);
|
||||
g.setValue("interface/min_players_multicolumn", 6);
|
||||
g.setValue("interface/deckeditorbannercardcomboboxvisible", false);
|
||||
// hand/table/replay/zoneview
|
||||
g.setValue("hand/horizontal", true);
|
||||
g.setValue("table/invert_vertical", true);
|
||||
g.setValue("replay/rewindBufferingMs", 6000);
|
||||
g.setValue("replay/fastForwardSpeed", 5);
|
||||
g.setValue("zoneview/groupby", 2);
|
||||
g.setValue("zoneview/sortby", 1);
|
||||
g.setValue("zoneview/pileview", false);
|
||||
|
||||
// paths
|
||||
g.beginGroup("paths");
|
||||
g.setValue("decks", "/custom/decks");
|
||||
g.setValue("pics", "/custom/pics");
|
||||
g.endGroup();
|
||||
|
||||
// visual deck storage (under [interface] group)
|
||||
g.setValue("interface/visualdeckstoragecardsize", 150);
|
||||
g.setValue("interface/visualdeckstoragesortingorder", 2);
|
||||
g.setValue("interface/visualdeckstorageshowfolders", false);
|
||||
g.setValue("interface/visualdeckstorageshowtagfilter", false);
|
||||
g.setValue("interface/visualdeckstoragedefaulttagslist", QStringList{"Alpha", "Beta"});
|
||||
g.setValue("interface/visualdeckstoragesearchfoldernames", false);
|
||||
g.setValue("interface/visualdeckstorageshowcoloridentity", false);
|
||||
g.setValue("interface/visualdeckstorageshowbannercardcombobox", false);
|
||||
g.setValue("interface/visualdeckstorageshowtagsondeckpreviews", false);
|
||||
g.setValue("interface/visualdeckstoragedrawunusedcoloridentities", false);
|
||||
g.setValue("interface/visualdeckstorageunusedcoloridentitiesopacity", 35);
|
||||
g.setValue("interface/visualdeckstoragetooltiptype", 1);
|
||||
g.setValue("interface/visualdeckstoragepromptforconversion", false);
|
||||
g.setValue("interface/visualdeckstoragealwaysconvert", true);
|
||||
g.setValue("interface/visualdeckstorageingame", false);
|
||||
g.setValue("interface/visualdeckstorageselectionanimation", false);
|
||||
g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsenabled", true);
|
||||
g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsamount", 25);
|
||||
|
||||
// card sizes (migrate into cards_display.ini)
|
||||
g.setValue("interface/visualdatabasedisplaycardsize", 80);
|
||||
g.setValue("interface/visualdeckeditorcardsize", 70);
|
||||
g.setValue("interface/edhreccardsize", 60);
|
||||
g.setValue("interface/archidektpreviewsize", 50);
|
||||
|
||||
g.sync();
|
||||
}
|
||||
|
||||
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Original global.ini should be backed up
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
|
||||
// Verify each per-file INI was created and has correct values
|
||||
ASSERT_TRUE(fileExists("tabs.ini"));
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/server"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/log"), QVariant(true));
|
||||
|
||||
ASSERT_TRUE(fileExists("sound.ini"));
|
||||
ASSERT_EQ(readFromIni("sound.ini", "sound/enabled"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("sound.ini", "sound/theme"), QVariant("custom_theme"));
|
||||
ASSERT_EQ(readFromIni("sound.ini", "sound/masterVolume"), QVariant(75));
|
||||
|
||||
ASSERT_TRUE(fileExists("game.ini"));
|
||||
ASSERT_EQ(readFromIni("game.ini", "game/maxPlayers"), QVariant(4));
|
||||
ASSERT_EQ(readFromIni("game.ini", "game/gameDescription"), QVariant("test game"));
|
||||
ASSERT_EQ(readFromIni("game.ini", "game/rememberGameSettings"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("game.ini", "game/gameTypes"), QVariant("commander"));
|
||||
ASSERT_EQ(readFromIni("game.ini", "game/onlyBuddies"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxPlayers"), QVariant(2));
|
||||
ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startingLifeTotal"), QVariant(40));
|
||||
|
||||
ASSERT_TRUE(fileExists("chat.ini"));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/mention"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/mentionColor"), QVariant("FF0000"));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/showMessagePopups"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/mentionCompleter"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/roomHistory"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/highlightColor"), QVariant("00FF00"));
|
||||
ASSERT_EQ(readFromIni("chat.ini", "chat/highlightWords"), QVariant("alpha beta"));
|
||||
|
||||
ASSERT_TRUE(fileExists("cache_storage.ini"));
|
||||
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/pixmapCacheSize"), QVariant(1024));
|
||||
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/networkCacheSize"), QVariant(2048));
|
||||
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/redirectCacheTtl"), QVariant(5));
|
||||
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/cardPictureLoaderCacheMethod"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/localCardImageStorageNamingScheme"), QVariant(2));
|
||||
|
||||
ASSERT_TRUE(fileExists("updates.ini"));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/startupUpdateCheck"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckPromptForUpdate"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckAlwaysUpdate"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(14));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/lastCardUpdateCheck"), QVariant(QDate(2024, 1, 1)));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/alwaysEnableNewSets"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/updateNotification"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/newVersionNotification"), QVariant(false));
|
||||
|
||||
ASSERT_TRUE(fileExists("personal.ini"));
|
||||
ASSERT_EQ(readFromIni("personal.ini", "personal/lang"), QVariant("de"));
|
||||
ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/showTips"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/seenTips"), QVariant(QStringList{"1", "2", "3"}));
|
||||
|
||||
ASSERT_TRUE(fileExists("downloads.ini"));
|
||||
ASSERT_EQ(readFromIni("downloads.ini", "downloads/pictureDownload"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("downloads.ini", "downloads/downloadSpoilers"), QVariant(true));
|
||||
|
||||
ASSERT_TRUE(fileExists("appearance.ini"));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("custom_theme"));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/maxFontSize"), QVariant(14));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/styleUserList"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundSource"), QVariant("custom_bg"));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundShuffleFrequency"), QVariant(30));
|
||||
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabDisplayCardName"), QVariant(false));
|
||||
|
||||
ASSERT_TRUE(fileExists("network.ini"));
|
||||
ASSERT_EQ(readFromIni("network.ini", "network/keepAlive"), QVariant(10));
|
||||
ASSERT_EQ(readFromIni("network.ini", "network/timeout"), QVariant(30));
|
||||
ASSERT_EQ(readFromIni("network.ini", "network/clientId"), QVariant("test-client-id"));
|
||||
ASSERT_EQ(readFromIni("network.ini", "network/clientVersion"), QVariant("test-client-version"));
|
||||
ASSERT_EQ(readFromIni("network.ini", "network/knownMissingFeatures"), QVariant("feature1,feature2"));
|
||||
|
||||
ASSERT_TRUE(fileExists("cards_display.ini"));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/roundCardCorners"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/overrideAllCardArtWithPersonalPreference"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/bumpSetsWithCardsInDeckToTop"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/includerebalancedcards"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/autoRotateSidewaysLayoutCards"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapAnimation"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/scaleCards"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/verticalCardOverlapPercent"), QVariant(42));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardInfoViewMode"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/printingSelector"), QVariant(90));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/sortOrder"), QVariant(3));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/navigationButtonsVisible"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckStorage"), QVariant(150));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDatabaseDisplay"), QVariant(80));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckEditor"), QVariant(70));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/edhrec"), QVariant(60));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/archidektPreview"), QVariant(50));
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/sampleHandSize"), QVariant(5));
|
||||
|
||||
ASSERT_TRUE(fileExists("card_counters.ini"));
|
||||
ASSERT_EQ(readFromIni("card_counters.ini", "cards/counters/0/color").toString(), QColor(Qt::red).name());
|
||||
ASSERT_FALSE(readFromIni("global.ini", "cards/counters/0/color").isValid());
|
||||
|
||||
ASSERT_TRUE(fileExists("interface.ini"));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/useTearOffMenus"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewInitialRowsMax"), QVariant(8));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewExpandedRowsMax"), QVariant(12));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/closeEmptyCardView"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/focusCardViewSearchBar"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/keepGameChatFocus"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/enabled"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/spectatorsEnabled"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/buddyConnectEnabled"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/doubleClickToPlay"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/clickPlaysAllSelected"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/playToStack"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/doNotDeleteArrowsInSubPhases"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/startingHandSize"), QVariant(5));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/annotateTokens"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/showLassoSelectionCount"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/showPersistentSelectionCount"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/tallyType"), QVariant(2));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/leftJustified"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/minPlayersMulticolumn"), QVariant(6));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/showStatusBar"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/showShortcuts"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "interface/showGameSelectorFilterToolbar"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "hand/horizontal"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "table/invertVertical"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "replay/rewindBufferingMs"), QVariant(6000));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "replay/fastForwardSpeed"), QVariant(5));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "zoneview/groupBy"), QVariant(2));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "zoneview/sortBy"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("interface.ini", "zoneview/pileView"), QVariant(false));
|
||||
|
||||
ASSERT_TRUE(fileExists("deck_editor.ini"));
|
||||
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/openDeckInNewTab"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/tagsWidgetVisible"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/defaultDeckEditorType"), QVariant(0));
|
||||
|
||||
ASSERT_TRUE(fileExists("paths.ini"));
|
||||
ASSERT_EQ(readFromIni("paths.ini", "paths/decks"), QVariant("/custom/decks"));
|
||||
ASSERT_EQ(readFromIni("paths.ini", "paths/pics"), QVariant("/custom/pics"));
|
||||
|
||||
ASSERT_TRUE(fileExists("visual_deck_storage.ini"));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/sortingOrder"), QVariant(2));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showFolders"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagFilter"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/defaultTagsList"),
|
||||
QVariant(QStringList{"Alpha", "Beta"}));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/searchFolderNames"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showColorIdentity"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showBannerCardComboBox"),
|
||||
QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagsOnDeckPreviews"),
|
||||
QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/drawUnusedColorIdentities"),
|
||||
QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/unusedColorIdentitiesOpacity"),
|
||||
QVariant(35));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/tooltipType"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/promptForConversion"),
|
||||
QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/alwaysConvert"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/inGame"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/selectionAnimation"),
|
||||
QVariant(false));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"),
|
||||
QVariant(true));
|
||||
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"),
|
||||
QVariant(25));
|
||||
|
||||
// No legacy flat keys should remain in the per-file INIs
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstoragecardsize"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/sampleHandSize"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/cardSize"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/notificationsenabled"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/printingselectorsortorder"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/visualDeckStorage/cardSize"));
|
||||
|
||||
// No legacy non-camelCase keys should remain in the per-file INIs
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "game/gamedescription"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "localgameoptions/maxplayers"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/roomhistory"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/highlightwords"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("sound.ini", "sound/mastervolume"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("downloads.ini", "downloads/picturedownload"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/keepalive"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/knownmissingfeatures"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("updates.ini", "updates/updatenotification"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/displaycardnames"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/cardinfoviewmode"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/usetearoffmenus"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/doubleclicktoplay"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/min_players_multicolumn"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "table/invert_vertical"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/groupby"));
|
||||
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/pileview"));
|
||||
|
||||
// Verify sentinel was written
|
||||
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, SecondCallDoesNothing)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("tabs/visualDeckStorage", true);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, SecondCallDoesNotOverwriteUserChanges)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("tabs/visualDeckStorage", true);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
// User changes the value after migration
|
||||
{
|
||||
QSettings tabsIni(settingsPath + "tabs.ini", QSettings::IniFormat);
|
||||
tabsIni.setValue("tabs/visualDeckStorage", false);
|
||||
tabsIni.sync();
|
||||
}
|
||||
|
||||
// Second migration should NOT overwrite the user's change
|
||||
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, PartialMigrationOnlyMigratesPresentKeys)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("tabs/visualDeckStorage", true);
|
||||
g.setValue("tabs/server", false);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
ASSERT_TRUE(fileExists("tabs.ini"));
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/server"), QVariant(false));
|
||||
|
||||
// Other files should NOT exist since no keys were present for them
|
||||
ASSERT_FALSE(fileExists("sound.ini"));
|
||||
ASSERT_FALSE(fileExists("game.ini"));
|
||||
ASSERT_FALSE(fileExists("chat.ini"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, KeyMapTranslationIsCorrect)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("personal/cardUpdateCheckInterval", 30);
|
||||
g.setValue("personal/updatereleasechannel", 1);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
ASSERT_TRUE(fileExists("updates.ini"));
|
||||
// The key should be translated from "personal/cardUpdateCheckInterval" to "updates/cardUpdateCheckInterval"
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(30));
|
||||
ASSERT_EQ(readFromIni("updates.ini", "updates/updateReleaseChannel"), QVariant(1));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("cards/displaycardnames", false);
|
||||
g.setValue("interface/deckeditorbannercardcomboboxvisible", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
ASSERT_TRUE(fileExists("cards_display.ini"));
|
||||
ASSERT_TRUE(fileExists("deck_editor.ini"));
|
||||
// "cards/displaycardnames" should be stored with its group prefix
|
||||
ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false));
|
||||
// deck editor keys belong to the deck editor settings now
|
||||
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(true));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent)
|
||||
{
|
||||
{
|
||||
QSettings nativeSettings;
|
||||
nativeSettings.setValue("server/previoushostlogin", "test_user");
|
||||
nativeSettings.setValue("server/auto_connect", true);
|
||||
nativeSettings.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
|
||||
|
||||
// Change the migrated value
|
||||
{
|
||||
QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat);
|
||||
serversIni.setValue("server/previousHostLogin", "modified_user");
|
||||
serversIni.sync();
|
||||
}
|
||||
|
||||
// Second migration should NOT overwrite the change
|
||||
ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath));
|
||||
ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("modified_user"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, LegacyMigrationCamelCasesKeys)
|
||||
{
|
||||
{
|
||||
QSettings nativeSettings;
|
||||
nativeSettings.setValue("sets/AAA/sortkey", 2);
|
||||
nativeSettings.setValue("sets/AAA/enabled", false);
|
||||
nativeSettings.setValue("sets/AAA/isknown", false);
|
||||
nativeSettings.setValue("server/previoushostlogin", "legacy_user");
|
||||
nativeSettings.setValue("server/auto_connect", 1);
|
||||
nativeSettings.setValue("server/fpport", "5080");
|
||||
nativeSettings.setValue("messages/count", 1);
|
||||
nativeSettings.setValue("messages/msg0", "hello");
|
||||
nativeSettings.setValue("filter_games/hide_full_games", true);
|
||||
nativeSettings.setValue("filter_games/min_players", 3);
|
||||
nativeSettings.setValue("filter_games/max_players", 5);
|
||||
nativeSettings.setValue("filter_games/game_type/deadbeef", true);
|
||||
nativeSettings.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
|
||||
|
||||
ASSERT_TRUE(fileExists("cardDatabase.ini"));
|
||||
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/sortKey"), QVariant(2));
|
||||
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/enabled"), QVariant(false));
|
||||
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/isKnown"), QVariant(false));
|
||||
|
||||
ASSERT_TRUE(fileExists("servers.ini"));
|
||||
ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("legacy_user"));
|
||||
ASSERT_EQ(readFromIni("servers.ini", "server/autoConnect"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("servers.ini", "server/fpPort"), QVariant("5080"));
|
||||
|
||||
ASSERT_TRUE(fileExists("messages.ini"));
|
||||
ASSERT_EQ(readFromIni("messages.ini", "messages/count"), QVariant(1));
|
||||
ASSERT_EQ(readFromIni("messages.ini", "messages/msg0"), QVariant("hello"));
|
||||
|
||||
ASSERT_TRUE(fileExists("gamefilters.ini"));
|
||||
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(true));
|
||||
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/minPlayers"), QVariant(3));
|
||||
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/maxPlayers"), QVariant(5));
|
||||
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameType/deadbeef"), QVariant(true));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, LegacyMigrationEmptyNativeFormatWritesSentinel)
|
||||
{
|
||||
QSettings().clear();
|
||||
QSettings().sync();
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
|
||||
ASSERT_TRUE(fileExists("personal.ini"));
|
||||
ASSERT_EQ(readFromIni("personal.ini", "migration/legacy_complete"), QVariant(true));
|
||||
ASSERT_FALSE(fileExists("messages.ini"));
|
||||
ASSERT_FALSE(fileExists("gamefilters.ini"));
|
||||
ASSERT_FALSE(fileExists("cardDatabase.ini"));
|
||||
|
||||
// Second call should be a no-op
|
||||
ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath));
|
||||
}
|
||||
|
||||
// --- migrateSettingsFromGlobalIni rename-failure test ---
|
||||
|
||||
TEST_F(SettingsMigrationTest, RenameFailureWithExistingBackup)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("tabs/visualDeckStorage", true);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
// Create a stale backup
|
||||
{
|
||||
QFile oldBackup(settingsPath + "global.ini.old");
|
||||
ASSERT_TRUE(oldBackup.open(QIODevice::WriteOnly));
|
||||
oldBackup.close();
|
||||
}
|
||||
|
||||
// Migration should still succeed
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
// Per-file INIs should be created
|
||||
ASSERT_TRUE(fileExists("tabs.ini"));
|
||||
ASSERT_TRUE(fileExists("sound.ini"));
|
||||
|
||||
// Sentinel should be set in the new global.ini
|
||||
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
|
||||
|
||||
// Original data should be backed up (stale backup was replaced)
|
||||
ASSERT_TRUE(fileExists("global.ini.old"));
|
||||
ASSERT_EQ(readFromIni("global.ini.old", "tabs/visualDeckStorage"), QVariant(true));
|
||||
|
||||
// Second call should be a no-op
|
||||
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
QCoreApplication app(argc, argv);
|
||||
app.setOrganizationName("CockatriceTest");
|
||||
app.setApplicationName("SettingsMigrationTest");
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
89
tests/warning_categories_test.cpp
Normal file
89
tests/warning_categories_test.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QString>
|
||||
#include <libcockatrice/utility/warning_categories.h>
|
||||
|
||||
TEST(WarningCategoriesTest, EmptyValueYieldsNoCategories)
|
||||
{
|
||||
EXPECT_TRUE(parseWarningCategories(QString()).isEmpty());
|
||||
EXPECT_TRUE(parseWarningCategories(QString("")).isEmpty());
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, PlainNamesDefaultToInterventionLevelOne)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("Flaming,Spamming,Causing Drama");
|
||||
|
||||
ASSERT_EQ(3, categories.size());
|
||||
EXPECT_EQ("Flaming", categories.at(0).name);
|
||||
EXPECT_EQ(1, categories.at(0).startingIl);
|
||||
EXPECT_EQ("Spamming", categories.at(1).name);
|
||||
EXPECT_EQ(1, categories.at(1).startingIl);
|
||||
EXPECT_EQ("Causing Drama", categories.at(2).name);
|
||||
EXPECT_EQ(1, categories.at(2).startingIl);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, ExplicitInterventionLevelsAreParsed)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("Cheating|2,Inappropriate Avatar|3");
|
||||
|
||||
ASSERT_EQ(2, categories.size());
|
||||
EXPECT_EQ("Cheating", categories.at(0).name);
|
||||
EXPECT_EQ(2, categories.at(0).startingIl);
|
||||
EXPECT_EQ("Inappropriate Avatar", categories.at(1).name);
|
||||
EXPECT_EQ(3, categories.at(1).startingIl);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, MixedEntriesKeepDefaultsForThoseWithoutLevels)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("Abusive Language|1,Cheating|2,Spamming");
|
||||
|
||||
ASSERT_EQ(3, categories.size());
|
||||
EXPECT_EQ(1, categories.at(0).startingIl);
|
||||
EXPECT_EQ(2, categories.at(1).startingIl);
|
||||
EXPECT_EQ("Spamming", categories.at(2).name);
|
||||
EXPECT_EQ(1, categories.at(2).startingIl);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, EmptyEntriesAreSkipped)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("Spamming,,Cheating|2,");
|
||||
|
||||
ASSERT_EQ(2, categories.size());
|
||||
EXPECT_EQ("Spamming", categories.at(0).name);
|
||||
EXPECT_EQ("Cheating", categories.at(1).name);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, WhitespaceIsTrimmed)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories(" Abusive Language , Cheating | 2 ");
|
||||
|
||||
ASSERT_EQ(2, categories.size());
|
||||
EXPECT_EQ("Abusive Language", categories.at(0).name);
|
||||
EXPECT_EQ(1, categories.at(0).startingIl);
|
||||
EXPECT_EQ("Cheating", categories.at(1).name);
|
||||
EXPECT_EQ(2, categories.at(1).startingIl);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, InvalidInterventionLevelsFallBackToOne)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("Spamming|abc,Cheating|0,Targeted Harassment|-3");
|
||||
|
||||
ASSERT_EQ(3, categories.size());
|
||||
EXPECT_EQ(1, categories.at(0).startingIl);
|
||||
EXPECT_EQ(1, categories.at(1).startingIl);
|
||||
EXPECT_EQ(1, categories.at(2).startingIl);
|
||||
}
|
||||
|
||||
TEST(WarningCategoriesTest, EntryWithOnlyLevelIsSkipped)
|
||||
{
|
||||
const QList<WarningCategory> categories = parseWarningCategories("|2,Spamming|2");
|
||||
|
||||
ASSERT_EQ(1, categories.size());
|
||||
EXPECT_EQ("Spamming", categories.at(0).name);
|
||||
EXPECT_EQ(2, categories.at(0).startingIl);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue