[Utility] Add warning categories parser with infraction levels

Parses the server's 'officialwarnings' setting (comma-separated, optional
'|IL' suffix) into WarningCategory structs so the client can display the
infraction level of each warning category. Includes GTest coverage.
This commit is contained in:
Lukas Brübach 2026-08-11 21:10:39 +02:00
parent 2b63bc46a7
commit 96ed16abee
5 changed files with 151 additions and 2 deletions

View file

@ -5,8 +5,9 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp set(UTILITY_SOURCES
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp
libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp
) )
set(UTILITY_HEADERS set(UTILITY_HEADERS
@ -24,6 +25,7 @@ set(UTILITY_HEADERS
libcockatrice/utility/zone_names.h libcockatrice/utility/zone_names.h
libcockatrice/utility/days_years_between.h libcockatrice/utility/days_years_between.h
libcockatrice/utility/server_rate_limiter.h libcockatrice/utility/server_rate_limiter.h
libcockatrice/utility/warning_categories.h
) )
add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})

View file

@ -0,0 +1,24 @@
#include "warning_categories.h"
QList<WarningCategory> parseWarningCategories(const QString &value)
{
QList<WarningCategory> categories;
const QStringList entries = value.split(',', Qt::SkipEmptyParts);
for (const QString &entry : entries) {
const QStringList parts = entry.split('|');
WarningCategory category;
category.name = parts.first().trimmed();
if (category.name.isEmpty()) {
continue;
}
if (parts.size() > 1) {
bool ok = false;
const int il = parts.at(1).trimmed().toInt(&ok);
if (ok && il > 0) {
category.startingIl = il;
}
}
categories.append(category);
}
return categories;
}

View file

@ -0,0 +1,28 @@
#ifndef WARNING_CATEGORIES_H
#define WARNING_CATEGORIES_H
#include <QList>
#include <QString>
/**
* A warning category the server offers to moderators, optionally carrying a
* recommended starting intervention level (see the moderator guide).
*/
struct WarningCategory
{
QString name;
int startingIl = 1;
};
/**
* Parses the `server/officialwarnings` setting value into warning categories.
*
* Entries are separated by commas. Each entry is a category name, optionally
* followed by "|" and the recommended starting intervention level:
* "Abusive Language|1,Cheating|2,Spamming"
* Entries without an explicit level default to intervention level 1.
* Empty entries are skipped.
*/
QList<WarningCategory> parseWarningCategories(const QString &value);
#endif // WARNING_CATEGORIES_H

View file

@ -11,6 +11,7 @@ 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_card_counter_test COMMAND server_card_counter_test)
add_test(NAME server_counter_test COMMAND server_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_rate_limiter_test COMMAND server_rate_limiter_test)
add_test(NAME warning_categories_test COMMAND warning_categories_test)
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5)
@ -27,6 +28,7 @@ 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_card_counter_test server_card_counter_test.cpp)
add_executable(server_counter_test server_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_rate_limiter_test server_rate_limiter_test.cpp)
add_executable(warning_categories_test warning_categories_test.cpp)
find_package(GTest) find_package(GTest)
@ -63,6 +65,7 @@ if(NOT GTEST_FOUND)
add_dependencies(server_card_counter_test gtest) add_dependencies(server_card_counter_test gtest)
add_dependencies(server_counter_test gtest) add_dependencies(server_counter_test gtest)
add_dependencies(server_rate_limiter_test gtest) add_dependencies(server_rate_limiter_test gtest)
add_dependencies(warning_categories_test gtest)
endif() endif()
include_directories(${GTEST_INCLUDE_DIRS}) include_directories(${GTEST_INCLUDE_DIRS})
@ -94,6 +97,9 @@ target_link_libraries(
target_link_libraries( target_link_libraries(
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
) )
target_link_libraries(
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
)
add_subdirectory(card_zone_algorithms) add_subdirectory(card_zone_algorithms)
add_subdirectory(carddatabase) add_subdirectory(carddatabase)

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