Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-09-12 18:34:48 +02:00 committed by GitHub
commit 6d7dadf657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
149 changed files with 2962 additions and 423 deletions

View file

@ -11,9 +11,11 @@ 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 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 deck_hash_performance_test COMMAND deck_hash_performance_test)
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
@ -28,10 +30,12 @@ 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(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)
# Find GTest
find_package(GTest)
@ -69,9 +73,11 @@ if(NOT GTEST_FOUND)
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(warning_categories_test gtest)
add_dependencies(lag_monitor_test gtest)
add_dependencies(latency_tracker_test gtest)
add_dependencies(metrics_registry_test gtest)
endif()
include_directories(${GTEST_INCLUDE_DIRS})
@ -100,6 +106,10 @@ target_link_libraries(
target_link_libraries(
server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${QT_MODULES_TEST}
)
target_link_libraries(
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
${QT_MODULES_TEST}
)
target_link_libraries(
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${QT_MODULES_TEST}
)
@ -110,6 +120,8 @@ target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES}
target_link_libraries(
latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${QT_MODULES_TEST}
)
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})
add_subdirectory(card_zone_algorithms)
add_subdirectory(carddatabase)

View file

@ -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;

View 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();
}

View file

@ -4,6 +4,8 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QPair>
#include <QSet>
#include <libcockatrice/card/format/format_legality_rules.h>
#include <libcockatrice/card/set/card_set.h>
@ -741,6 +743,135 @@ TEST_F(OracleImporterTest, StartImportParsesSetsLazily)
ASSERT_FALSE(importer->getCardList().value("Lazy Import Card").isNull());
}
// ============================================================================
// Scan progress reporting tests
// ============================================================================
TEST(OracleScanProgress, ScanProgressReportsMonotonicBytesToTotal)
{
QJsonObject setObj;
setObj["code"] = "tst";
setObj["name"] = "Test Set";
setObj["type"] = "expansion";
setObj["releaseDate"] = "2024-01-01";
QJsonArray cards;
for (int i = 0; i < 40; ++i) {
QJsonObject card;
card["name"] = QString("Card %1").arg(i);
card["text"] = "Some rules text used to bulk up the card payload.";
card["layout"] = "normal";
cards.append(card);
}
setObj["cards"] = cards;
QJsonObject root;
root["data"] = QJsonObject{{"TST", setObj}};
const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact);
QList<QPair<qsizetype, qsizetype>> reports;
RawJson::ScanError error;
const QList<RawJson::SetRange> ranges =
RawJson::scanSetRanges(data, &error, [&reports](qsizetype bytesRead, qsizetype totalBytes) {
reports.append({bytesRead, totalBytes});
});
ASSERT_FALSE(error.isError()) << error.message.toStdString();
ASSERT_EQ(ranges.size(), 1);
ASSERT_FALSE(reports.isEmpty());
ASSERT_GT(reports.size(), 1);
qsizetype last = 0;
for (const auto &[bytesRead, totalBytes] : reports) {
ASSERT_EQ(totalBytes, data.size());
ASSERT_GE(bytesRead, last) << "scan progress must be monotonic";
ASSERT_LE(bytesRead, totalBytes) << "scan progress must not overshoot the document size";
last = bytesRead;
}
ASSERT_EQ(reports.constLast().first, data.size()) << "scan must end at 100%";
ASSERT_LE(reports.size(), 160) << "scan reports must be throttled";
}
TEST(OracleScanProgress, ScanWithoutCallbackStillParses)
{
QJsonObject setObj;
setObj["code"] = "tst";
setObj["name"] = "Test Set";
setObj["type"] = "expansion";
setObj["releaseDate"] = "2024-01-01";
setObj["cards"] = QJsonArray();
QJsonObject root;
root["data"] = QJsonObject{{"TST", setObj}};
const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact);
RawJson::ScanError error;
const QList<RawJson::SetRange> ranges = RawJson::scanSetRanges(data, &error);
ASSERT_FALSE(error.isError()) << error.message.toStdString();
ASSERT_EQ(ranges.size(), 1);
ASSERT_EQ(ranges.first().code, "tst");
}
TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmitsScanProgress)
{
QJsonObject setObj;
setObj["code"] = "tst";
setObj["name"] = "Test Set";
setObj["type"] = "expansion";
setObj["releaseDate"] = "2024-01-01";
QJsonArray cards;
for (int i = 0; i < 40; ++i) {
QJsonObject card;
card["name"] = QString("Card %1").arg(i);
cards.append(card);
}
setObj["cards"] = cards;
QJsonObject root;
root["data"] = QJsonObject{{"TST", setObj}};
const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact);
QList<QPair<qsizetype, qsizetype>> emissions;
QObject::connect(importer, &OracleImporter::dataReadProgress,
[&emissions](int bytesRead, int totalBytes) { emissions.append({bytesRead, totalBytes}); });
ASSERT_TRUE(importer->readSetsFromByteArray(data));
ASSERT_FALSE(emissions.isEmpty());
for (const auto &[bytesRead, totalBytes] : emissions) {
ASSERT_EQ(totalBytes, data.size());
ASSERT_GE(bytesRead, 0);
ASSERT_LE(bytesRead, totalBytes);
}
ASSERT_EQ(emissions.constLast().first, data.size());
}
TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions)
{
QJsonObject setObj;
setObj["code"] = "tst";
setObj["name"] = "Test Set";
setObj["type"] = "expansion";
setObj["releaseDate"] = "2024-01-01";
setObj["cards"] = QJsonArray();
QJsonObject root;
root["data"] = QJsonObject{{"TST", setObj}};
const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact);
int emissions = 0;
QObject::connect(importer, &OracleImporter::dataReadProgress, [&emissions](int, int) { ++emissions; });
importer->setProgressReporting(false);
ASSERT_TRUE(importer->readSetsFromByteArray(data));
ASSERT_EQ(emissions, 0);
importer->setProgressReporting(true);
ASSERT_TRUE(importer->readSetsFromByteArray(data));
ASSERT_GT(emissions, 0);
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);

View 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();
}

View file

@ -300,6 +300,12 @@ TEST_F(SettingsDefaultsTest, Chat_RoomHistory_Default)
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)