mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-13 13:30:00 -08:00
Merge branch 'Cockatrice:master' into feature/continuumatrice-product-migration
This commit is contained in:
commit
8b9e79758b
69 changed files with 2093 additions and 478 deletions
|
|
@ -357,6 +357,18 @@ cockatrice_add_test(
|
|||
LABELS
|
||||
ci
|
||||
)
|
||||
# The local game runs one client and one server in the same process.
|
||||
# Either of them can be destroyed first; the survivor has to stay usable.
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
local_client_lifetime_test
|
||||
SOURCES
|
||||
local_client_lifetime_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
LABELS
|
||||
ci
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_room_channels_test
|
||||
|
|
@ -407,23 +419,6 @@ cockatrice_add_test(
|
|||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loader_local_matching_test
|
||||
SOURCES
|
||||
${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
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
libcockatrice_settings
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_hash_performance_test
|
||||
|
|
@ -475,6 +470,19 @@ endif()
|
|||
|
||||
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
||||
if(WITH_CLIENT)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
moxfield_signature_spells_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/deck_loader/card_node_function.cpp
|
||||
moxfield_signature_spells_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
single_instance_manager_test
|
||||
|
|
@ -543,3 +551,46 @@ endif()
|
|||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
add_subdirectory(picture_loader_benchmark)
|
||||
endif()
|
||||
|
||||
# loader_local_matching_test and deck_state_manager_test links libcockatrice_settings,
|
||||
# which only exists when a client/UI-capable target is being built.
|
||||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
set(SETTINGS_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.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
|
||||
${VERSION_STRING_CPP}
|
||||
)
|
||||
set(SETTINGS_LIBS libcockatrice_network libcockatrice_settings)
|
||||
set(SETTINGS_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loader_local_matching_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
${SETTINGS_SOURCES}
|
||||
loader_local_matching_test.cpp
|
||||
LIBS
|
||||
${SETTINGS_LIBS}
|
||||
INCLUDE_DIRS
|
||||
${SETTINGS_INCLUDE_DIRS}
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_state_manager_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||
${SETTINGS_SOURCES}
|
||||
deck_state_manager_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_models
|
||||
libcockatrice_utility
|
||||
${SETTINGS_LIBS}
|
||||
INCLUDE_DIRS
|
||||
${SETTINGS_INCLUDE_DIRS}
|
||||
)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,21 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
|||
LIBS
|
||||
libcockatrice_filters
|
||||
)
|
||||
|
||||
# ------------------------
|
||||
# Visual Display Model Test
|
||||
# ------------------------
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
visual_display_model_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
visual_display_model_test.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_models
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
|
|
|
|||
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* @file visual_display_model_test.cpp
|
||||
* @brief Tests for the flattened model behind the visual card database grid.
|
||||
*/
|
||||
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/card/set/card_set.h"
|
||||
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
||||
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
||||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QEventLoop>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
// GTest's main() creates no QCoreApplication, which QTimer needs for its event loop.
|
||||
const QCoreApplication &application()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char executable[] = "visual_display_model_test";
|
||||
static char *argv[] = {executable, nullptr};
|
||||
static const QCoreApplication app(argc, argv);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Lets the rebuild timer fire, as it would in the running client.
|
||||
void waitForRebuild()
|
||||
{
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(100, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
class CardDatabaseVisualDisplayModelTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
(void)application();
|
||||
|
||||
db = std::make_unique<CardDatabase>(nullptr, new NoopCardPreferenceProvider(),
|
||||
new TestCardDatabasePathProvider(), new NoopCardSetPriorityController());
|
||||
db->loadCardDatabases();
|
||||
|
||||
cardModel = std::make_unique<CardDatabaseModel>(db.get(), false);
|
||||
displayModel = std::make_unique<CardDatabaseDisplayModel>();
|
||||
displayModel->setSourceModel(cardModel.get());
|
||||
visualModel = std::make_unique<CardDatabaseVisualDisplayModel>(displayModel.get(), db->query());
|
||||
visualModel->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds a card with @p printingCount printings in each named set.
|
||||
* @param printingCountPerSet Set short name to the number of printings in that set.
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr makeCard(const QString &name, const QMap<QString, int> &printingCountPerSet)
|
||||
{
|
||||
SetToPrintingsMap setsInfo;
|
||||
for (auto it = printingCountPerSet.constBegin(); it != printingCountPerSet.constEnd(); ++it) {
|
||||
CardSetPtr set = CardSet::newInstance(&setController, it.key(), it.key() + " test set", "expansion",
|
||||
QDate(), CardSet::PriorityPrimary);
|
||||
for (int printing = 0; printing < it.value(); ++printing) {
|
||||
QHash<QString, QString> properties;
|
||||
properties["uuid"] = QString("%1-%2-%3").arg(it.key(), name).arg(printing);
|
||||
setsInfo[it.key()].append(PrintingInfo(set, LazyPropertiesHash(properties)));
|
||||
}
|
||||
}
|
||||
|
||||
CardInfo::UiAttributes attributes = {.tableRow = 1};
|
||||
return CardInfo::newInstance(name, "", false, {}, {}, {}, setsInfo, attributes);
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController setController;
|
||||
std::unique_ptr<CardDatabase> db;
|
||||
std::unique_ptr<CardDatabaseModel> cardModel;
|
||||
std::unique_ptr<CardDatabaseDisplayModel> displayModel;
|
||||
std::unique_ptr<CardDatabaseVisualDisplayModel> visualModel;
|
||||
};
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ExposesTheCardInfoBehindAProxyRow)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
|
||||
const QVariant info = displayModel->data(displayModel->index(0, CardDatabaseModel::NameColumn),
|
||||
CardDatabaseDisplayModel::CardInfoRole);
|
||||
ASSERT_TRUE(info.isValid());
|
||||
EXPECT_FALSE(info.value<CardInfoPtr>().isNull());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, FlattensOneRowPerProxyCardWithoutSetFilters)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
EXPECT_EQ(displayModel->rowCount(), visualModel->rowCount());
|
||||
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ(
|
||||
displayModel->data(displayModel->index(row, CardDatabaseModel::NameColumn), Qt::DisplayRole).toString(),
|
||||
card.getName())
|
||||
<< "row " << row;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsOneRowPerCardWhenNoSetIsFiltered)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
EXPECT_EQ("Multi", visualModel->cardAt(visualModel->rowCount() - 1).getName());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsEveryPrintingOfAFilteredSet)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ("Multi", card.getName());
|
||||
ASSERT_FALSE(card.getPrinting().isEmpty()) << "row " << row;
|
||||
EXPECT_EQ("TST", card.getPrinting().getSet()->getShortName()) << "row " << row;
|
||||
}
|
||||
EXPECT_NE(visualModel->cardAt(0).getPrinting(), visualModel->cardAt(1).getPrinting());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, HidesCardsWithoutAPrintingInTheFilteredSets)
|
||||
{
|
||||
visualModel->setSetFilterTerms({"NO SUCH SET"});
|
||||
|
||||
EXPECT_EQ(0, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RestoresOneRowPerCardWhenTheSetTermsClear)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
|
||||
visualModel->setSetFilterTerms({});
|
||||
EXPECT_EQ(rowsBefore, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RebuildsByItselfWhenTheProxyGainsARow)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Late", {{"TST", 1}}));
|
||||
waitForRebuild();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ReportsTheCardNameForDisplayAndAccessibleText)
|
||||
{
|
||||
ASSERT_GT(visualModel->rowCount(), 0);
|
||||
const QString expected = visualModel->cardAt(0).getName();
|
||||
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::DisplayRole).toString());
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::AccessibleTextRole).toString());
|
||||
EXPECT_FALSE(visualModel->data(QModelIndex(), Qt::DisplayRole).isValid());
|
||||
}
|
||||
} // namespace
|
||||
190
tests/deck_state_manager_test.cpp
Normal file
190
tests/deck_state_manager_test.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/** @file deck_state_manager_test.cpp
|
||||
* @brief Tests for DeckStateManager operations.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <client/settings/cache_settings.h>
|
||||
#include <deck_list_model.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <interface/widgets/deck_editor/deck_state_manager.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Assert that the DeckStateManager's deck list contains only the given zone,
|
||||
* and that that zone only contains the given number of cards, all with the given card name.
|
||||
*
|
||||
* @param manager the DeckStateManager
|
||||
* @param restrictToZones In which zones to look for the zone and cards. Empty means use the entire deck
|
||||
* @param zone The only zone that should exist in the deck
|
||||
* @param cardName The name of the only card that should exist in the deck
|
||||
* @param cardNumber The number of cards that should be in the deck
|
||||
*/
|
||||
static void containsExactly(const DeckStateManager &manager,
|
||||
const QSet<QString> &restrictToZones,
|
||||
QString zone,
|
||||
QString cardName,
|
||||
int cardNumber)
|
||||
{
|
||||
QList<const InnerDecklistNode *> zones = manager.getDeckList().getZoneNodes(restrictToZones);
|
||||
ASSERT_EQ(zones.size(), 1);
|
||||
ASSERT_EQ(zones[0]->getName(), zone);
|
||||
ASSERT_EQ(zones[0]->size(), 1);
|
||||
const DecklistCardNode *card = dynamic_cast<const DecklistCardNode *>((*zones[0])[0]);
|
||||
ASSERT_NE(card, nullptr);
|
||||
ASSERT_EQ(card->getName(), cardName);
|
||||
ASSERT_EQ(card->getNumber(), cardNumber);
|
||||
}
|
||||
|
||||
/** @brief Return CardInfo of a token with the given name. */
|
||||
static CardInfoPtr makeToken(QString name)
|
||||
{
|
||||
return CardInfo::newInstance(name, "", true, {}, {}, {}, {}, {});
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// addCard
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckStateManager, AddCards)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, AddCardZones)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Grizzly Bears")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), DECK_ZONE_MAYBEBOARD);
|
||||
|
||||
// Check if the cards got put into the correct zones.
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 3);
|
||||
containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Grizzly Bears", 1);
|
||||
containsExactly(manager, {DECK_ZONE_SIDE}, DECK_ZONE_SIDE, "Lightning Bolt", 1);
|
||||
containsExactly(manager, {DECK_ZONE_MAYBEBOARD}, DECK_ZONE_MAYBEBOARD, "Swords to Plowshares", 1);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, AddTokens)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAYBEBOARD);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
|
||||
// A token should always be put into the tokens zone.
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 4);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// decrementCard
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckStateManager, DecrementCards)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
|
||||
// Removing from the sideboard should do nothing, as there is nothing in the side board.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
|
||||
// Remove the first card.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1);
|
||||
|
||||
// Removing the second card should also remove the zone.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, DecrementTokens)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 3);
|
||||
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 2);
|
||||
|
||||
// Removing tokens from a given zone should remove them from the
|
||||
// token zone if the given zone does not contain the token.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, DecrementTokenMainboard)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
|
||||
// Add cards to the deck list directly so tokens are not
|
||||
// automatically added to the token zone.
|
||||
manager.getDeckListShared()->addCard("Beast", DECK_ZONE_MAIN);
|
||||
manager.getDeckListShared()->addCard("Beast", DECK_ZONE_TOKENS);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 2);
|
||||
containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Beast", 1);
|
||||
containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
// decrementCard uses the model to find the card, so we need to rebuild it.
|
||||
manager.getModel()->rebuildTree();
|
||||
|
||||
// Tokens should be removed from the main zone first.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes({DECK_ZONE_MAIN}).size(), 0);
|
||||
containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
// If the main zone is empty they can be removed from the token zone.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Redirect SettingsCache reads/writes (app-data location) away from the real user profile.
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("CockatriceTest"));
|
||||
QCoreApplication::setApplicationName(
|
||||
QStringLiteral("deck-state-manager-test-%1").arg(QCoreApplication::applicationPid()));
|
||||
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// Add cards to the card database
|
||||
auto *db = CardDatabaseManager::getInstance();
|
||||
db->addCard(CardInfo::newInstance("Lightning Bolt"));
|
||||
db->addCard(CardInfo::newInstance("Grizzly Bears"));
|
||||
db->addCard(CardInfo::newInstance("Swords to Plowshares"));
|
||||
db->addCard(makeToken("Beast"));
|
||||
|
||||
int result = RUN_ALL_TESTS();
|
||||
|
||||
// Clean up the settings directory in ~/.qttest if the tests ran.
|
||||
if (!::testing::GTEST_FLAG(list_tests)) {
|
||||
QDir(SettingsCache::instance().getDataPath()).removeRecursively();
|
||||
QDir(SettingsCache::instance().getCachePath()).removeRecursively();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
149
tests/local_client_lifetime_test.cpp
Normal file
149
tests/local_client_lifetime_test.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* @file local_client_lifetime_test.cpp
|
||||
* @brief Tests that a LocalClient survives the death of the LocalServer it talks to.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QMessageLogContext>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/client/local/local_client.h>
|
||||
#include <libcockatrice/network/server/local/local_server.h>
|
||||
#include <libcockatrice/network/server/local/local_server_interface.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <memory>
|
||||
|
||||
// The server library refers to this global; the real applications define it in main.cpp.
|
||||
RNG_Abstract *rng = nullptr;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// GTest's main() creates no QCoreApplication, which QObject::deleteLater() needs.
|
||||
const QCoreApplication &application()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char executable[] = "local_client_lifetime_test";
|
||||
static char *argv[] = {executable, nullptr};
|
||||
static const QCoreApplication app(argc, argv);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Collects the warnings one logging category emits while this object is alive.
|
||||
///
|
||||
/// The handler is process-global and Qt does not nest handlers, so only one capture can be
|
||||
/// installed at a time; GTest running one case at a time is what makes that safe.
|
||||
class CategoryWarningCapture
|
||||
{
|
||||
public:
|
||||
explicit CategoryWarningCapture(const char *_category) : category(_category)
|
||||
{
|
||||
previousHandler = qInstallMessageHandler(captureMessage);
|
||||
s_active = this;
|
||||
}
|
||||
|
||||
~CategoryWarningCapture()
|
||||
{
|
||||
qInstallMessageHandler(previousHandler);
|
||||
s_active = nullptr;
|
||||
}
|
||||
|
||||
CategoryWarningCapture(const CategoryWarningCapture &) = delete;
|
||||
CategoryWarningCapture &operator=(const CategoryWarningCapture &) = delete;
|
||||
|
||||
const QStringList &warnings() const
|
||||
{
|
||||
return capturedWarnings;
|
||||
}
|
||||
|
||||
private:
|
||||
static void captureMessage(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
if (s_active != nullptr && type == QtWarningMsg && context.category != nullptr &&
|
||||
std::strcmp(s_active->category, context.category) == 0) {
|
||||
s_active->capturedWarnings.append(message);
|
||||
}
|
||||
|
||||
// Forward everything else, so a failure elsewhere in the test is still visible.
|
||||
if (previousHandler != nullptr) {
|
||||
previousHandler(type, context, message);
|
||||
return;
|
||||
}
|
||||
std::fprintf(stderr, "%s\n", qPrintable(qFormatLogMessage(type, context, message)));
|
||||
}
|
||||
|
||||
const char *category;
|
||||
QStringList capturedWarnings;
|
||||
|
||||
static CategoryWarningCapture *s_active;
|
||||
static QtMessageHandler previousHandler;
|
||||
};
|
||||
|
||||
CategoryWarningCapture *CategoryWarningCapture::s_active = nullptr;
|
||||
QtMessageHandler CategoryWarningCapture::previousHandler = nullptr;
|
||||
|
||||
class LocalClientLifetimeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
(void)application();
|
||||
|
||||
server = std::make_unique<LocalServer>();
|
||||
LocalServerInterface *lsi = server->newConnection();
|
||||
ASSERT_NE(lsi, nullptr);
|
||||
// Building the client logs in and joins room 0 through that interface, exactly as
|
||||
// MainWindow::startLocalGame() does.
|
||||
client = std::make_unique<LocalClient>(lsi, QStringLiteral("lifetime-test-player"),
|
||||
QStringLiteral("lifetime-test-client-id"));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
// Either case may already have deleted one of these.
|
||||
client.reset();
|
||||
server.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<LocalServer> server;
|
||||
std::unique_ptr<LocalClient> client;
|
||||
};
|
||||
|
||||
TEST_F(LocalClientLifetimeTest, SendAfterServerDeletedIsDropped)
|
||||
{
|
||||
server.reset(); // also destroys the LocalServerInterface the client was built on
|
||||
|
||||
CategoryWarningCapture capture("local_client");
|
||||
// A default container stands in for a command still queued when the local game ended.
|
||||
client->sendCommandContainer(CommandContainer());
|
||||
|
||||
ASSERT_EQ(capture.warnings().size(), 1);
|
||||
EXPECT_TRUE(capture.warnings().first().contains(QStringLiteral("dropping command")))
|
||||
<< capture.warnings().first().toStdString();
|
||||
|
||||
// The surviving client is intact: reading its state must not touch the deleted interface.
|
||||
EXPECT_EQ(client->getUserName(), QStringLiteral("lifetime-test-player"));
|
||||
}
|
||||
|
||||
TEST_F(LocalClientLifetimeTest, DestroyingClientAfterServerIsSafe)
|
||||
{
|
||||
server.reset();
|
||||
|
||||
CategoryWarningCapture capture("local_client");
|
||||
client.reset(); // must neither crash nor touch the destroyed interface
|
||||
|
||||
// The destructor itself must have been silent.
|
||||
EXPECT_TRUE(capture.warnings().isEmpty());
|
||||
|
||||
// A working capture proves the emptiness above is real rather than a broken handler.
|
||||
qCWarning(LocalClientLog) << "local_client_lifetime_test capture self-check";
|
||||
EXPECT_EQ(capture.warnings().size(), 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
84
tests/moxfield_signature_spells_test.cpp
Normal file
84
tests/moxfield_signature_spells_test.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#include "client/network/parsers/interface_json_deck_parser.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
TEST(MoxfieldImport, SignatureSpellIsAddedToMainWithoutReplacingCommanderBanner)
|
||||
{
|
||||
// Moxfield v2 board objects map IDs to entries with quantity and card.
|
||||
const auto input = QJsonDocument::fromJson(R"json({
|
||||
"name": "Oathbreaker example", "description": "Notes retained",
|
||||
"mainboard": {"land": {"quantity": 1, "card": {"name": "Island", "set": "abc", "cn": "1"}}},
|
||||
"sideboard": {"side": {"quantity": 1, "card": {"name": "Mountain", "set": "abc", "cn": "2"}}},
|
||||
"commanders": {"walker": {"quantity": 1, "card": {"name": "Jace", "set": "abc", "cn": "3", "scryfall_id": "walker-id"}}},
|
||||
"signatureSpells": {"spell": {"quantity": 1, "card": {"name": "Lightning Bolt", "set": "xyz", "cn": "4", "scryfall_id": "spell-id"}}}
|
||||
})json")
|
||||
.object();
|
||||
MoxfieldJsonParser parser;
|
||||
DeckList deck = parser.parse(input);
|
||||
int mainSpellCount = 0;
|
||||
int sideSpellCount = 0;
|
||||
int mainLands = 0;
|
||||
int sideLands = 0;
|
||||
deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
if (card->getName() == "Island" && zone->getName() == DECK_ZONE_MAIN) {
|
||||
mainLands += card->getNumber();
|
||||
}
|
||||
if (card->getName() == "Mountain" && zone->getName() == DECK_ZONE_SIDE) {
|
||||
sideLands += card->getNumber();
|
||||
}
|
||||
if (card->getName() != "Lightning Bolt") {
|
||||
return;
|
||||
}
|
||||
if (zone->getName() == DECK_ZONE_MAIN) {
|
||||
mainSpellCount += card->getNumber();
|
||||
EXPECT_EQ(card->getCardSetShortName(), "XYZ");
|
||||
EXPECT_EQ(card->getCardCollectorNumber(), "4");
|
||||
EXPECT_EQ(card->getCardProviderId(), "spell-id");
|
||||
} else if (zone->getName() == DECK_ZONE_SIDE) {
|
||||
sideSpellCount += card->getNumber();
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(mainSpellCount, 1);
|
||||
EXPECT_EQ(sideSpellCount, 0);
|
||||
EXPECT_EQ(mainLands, 1);
|
||||
EXPECT_EQ(sideLands, 1);
|
||||
EXPECT_EQ(deck.getName(), "Oathbreaker example");
|
||||
EXPECT_EQ(deck.getComments(), "Notes retained");
|
||||
EXPECT_EQ(deck.getBannerCard().name, "Jace");
|
||||
EXPECT_EQ(deck.getBannerCard().providerId, "walker-id");
|
||||
}
|
||||
|
||||
TEST(MoxfieldImport, QuantityAwareDeckInsertionRetainsCountAndPrinting)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *card = deck.addCards("Island", 2, DECK_ZONE_MAIN, -1, "ABC", "4", "id");
|
||||
ASSERT_NE(card, nullptr);
|
||||
EXPECT_EQ(card->getNumber(), 2);
|
||||
EXPECT_EQ(card->getCardProviderId(), "id");
|
||||
EXPECT_EQ(deck.addCards("Island", 0, DECK_ZONE_MAIN), nullptr);
|
||||
}
|
||||
|
||||
TEST(OnlineImport, ArchidektPreservesNameDescriptionAndCards)
|
||||
{
|
||||
const auto input = QJsonDocument::fromJson(R"json({
|
||||
"name": "Online deck", "description": "Deck notes",
|
||||
"cards": [{"quantity": 2, "card": {
|
||||
"oracleCard": {"name": "Island"}, "edition": {"editioncode": "abc"}, "collectorNumber": "1"
|
||||
}}]
|
||||
})json")
|
||||
.object();
|
||||
ArchidektJsonParser parser;
|
||||
const DeckList deck = parser.parse(input);
|
||||
int count = 0;
|
||||
deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
if (zone->getName() == DECK_ZONE_MAIN && card->getName() == "Island") {
|
||||
count += card->getNumber();
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(count, 2);
|
||||
EXPECT_EQ(deck.getName(), "Online deck");
|
||||
EXPECT_EQ(deck.getComments(), "Deck notes");
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
#include "game/game_config.h"
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_game.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "server.h"
|
||||
|
|
@ -119,13 +120,13 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isJudge = true}, false), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "secret", {.isJudge = true}, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
|
||||
|
|
@ -133,9 +134,9 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, false), Response::RespUserLevelTooLow);
|
||||
// An unregistered judge may enter when overriding restrictions.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
|
||||
|
|
@ -143,10 +144,10 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, QString(), {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
|
||||
|
|
@ -154,11 +155,11 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isSpectator = true}, 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
|
||||
|
|
@ -166,8 +167,8 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isJudge = true}, true), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {.isJudge = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue