4392: Add SQL/Score tests r=heinrich5991 a=def-

<!-- What is the motivation for the changes of this pull request -->

## Checklist

- [ ] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: def <dennis@felsin9.de>
This commit is contained in:
bors[bot] 2021-12-20 00:42:03 +00:00 committed by GitHub
commit 3013466b86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 2561 additions and 1991 deletions

View file

@ -32,6 +32,7 @@ jobs:
env:
CFLAGS: -Wdeclaration-after-statement -Werror
CXXFLAGS: -Werror
GTEST_FILTER: -*SQLite*
- os: macOS-latest
cmake-args: -G "Unix Makefiles"
build-args: --parallel
@ -63,7 +64,17 @@ jobs:
- name: Prepare Linux (fancy)
if: contains(matrix.os, 'ubuntu') && matrix.fancy
run: |
sudo apt-get install libmariadbclient-dev libwebsockets-dev -y
sudo apt-get install libmariadbclient-dev libwebsockets-dev mariadb-server -y
sudo rm -rf /var/lib/mysql/
sudo mysql_install_db --user=mysql --datadir=/var/lib/mysql/
cd /usr; sudo /usr/bin/mysqld_safe --datadir='/var/lib/mysql/' --no-watch
sleep 10
sudo mariadb <<EOF
CREATE DATABASE ddnet;
CREATE USER 'ddnet'@'localhost' IDENTIFIED BY 'thebestpassword';
GRANT ALL PRIVILEGES ON ddnet.* TO 'ddnet'@'localhost';
FLUSH PRIVILEGES;
EOF
- name: Prepare macOS
if: contains(matrix.os, 'macOS')
@ -82,6 +93,7 @@ jobs:
${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=Debug -Werror=dev -DDOWNLOAD_GTEST=ON -DDEV=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG=. ..
${{ matrix.cmake-path }}cmake --build . --config Debug --target everything ${{ matrix.build-args }}
- name: Test debug
env: ${{ matrix.env }}
run: |
cd debug
${{ matrix.cmake-path }}cmake --build . --config Debug --target run_tests ${{ matrix.build-args }}
@ -95,6 +107,7 @@ jobs:
${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=Release -Werror=dev -DDOWNLOAD_GTEST=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=. ..
${{ matrix.cmake-path }}cmake --build . --config Release --target everything ${{ matrix.build-args }}
- name: Test release
env: ${{ matrix.env }}
run: |
cd release
${{ matrix.cmake-path }}cmake --build . --config Release --target run_tests ${{ matrix.build-args }}
@ -120,12 +133,13 @@ jobs:
run: |
mkdir fancy
cd fancy
${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DANTIBOT=ON -DMYSQL=ON -DWEBSOCKETS=ON ..
${{ matrix.cmake-path }}cmake ${{ matrix.cmake-args }} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDOWNLOAD_GTEST=ON -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=. -DANTIBOT=ON -DTEST_MYSQL=ON -DWEBSOCKETS=ON ..
${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target everything ${{ matrix.build-args }}
- name: Test fancy
if: matrix.fancy
env: ${{ matrix.env }}
run: |
cd release
cd fancy
${{ matrix.cmake-path }}cmake --build . --config RelWithDebInfo --target run_tests ${{ matrix.build-args }}
./DDNet-Server shutdown

View file

@ -101,6 +101,7 @@ endif()
option(WEBSOCKETS "Enable websockets support" OFF)
option(MYSQL "Enable mysql support" OFF)
option(TEST_MYSQL "Test mysql support in unit tests (also sets -DMYSQL=ON)" OFF)
option(AUTOUPDATE "Enable the autoupdater" OFF)
option(INFORM_UPDATE "Inform about available updates" ON)
option(VIDEORECORDER "Enable video recording support via FFmpeg" OFF)
@ -117,6 +118,10 @@ option(DISCORD_DYNAMIC "Enable discovering Discord rich presence libraries at ru
option(PREFER_BUNDLED_LIBS "Prefer bundled libraries over system libraries" ${AUTO_DEPENDENCIES_DEFAULT})
option(DEV "Don't generate stuff necessary for packaging" OFF)
if(TEST_MYSQL)
set(MYSQL ON)
endif()
# Set version if not explicitly set
if(NOT VERSION)
set(VERSION ${PROJECT_VERSION})
@ -2112,6 +2117,8 @@ if(SERVER)
save.h
score.cpp
score.h
scoreworker.cpp
scoreworker.h
teams.cpp
teams.h
teehistorian.cpp
@ -2271,6 +2278,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
netaddr.cpp
packer.cpp
prng.cpp
score.cpp
secure_random.cpp
serverbrowser.cpp
serverinfo.cpp
@ -2296,10 +2304,18 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
src/engine/client/serverbrowser_ping_cache.cpp
src/engine/client/serverbrowser_ping_cache.h
src/engine/client/sqlite.cpp
src/engine/server/databases/connection.cpp
src/engine/server/databases/connection.h
src/engine/server/databases/sqlite.cpp
src/engine/server/databases/mysql.cpp
src/engine/server/name_ban.cpp
src/engine/server/name_ban.h
src/engine/server/sql_string_helpers.cpp
src/engine/server/sql_string_helpers.h
src/game/server/teehistorian.cpp
src/game/server/teehistorian.h
src/game/server/scoreworker.cpp
src/game/server/scoreworker.h
)
set(TARGET_TESTRUNNER testrunner)
@ -2310,7 +2326,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST)
$<TARGET_OBJECTS:game-shared>
${DEPS}
)
target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${CURL_LIBRARIES} ${GTEST_LIBRARIES})
target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${CURL_LIBRARIES} ${MYSQL_LIBRARIES} ${GTEST_LIBRARIES})
target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${CURL_INCLUDE_DIRS} ${GTEST_INCLUDE_DIRS})
list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER})
@ -2805,9 +2821,12 @@ foreach(target ${TARGETS_OWN})
target_compile_definitions(${target} PRIVATE CONF_HEADLESS_CLIENT)
endif()
if(MYSQL)
target_compile_definitions(${target} PRIVATE CONF_SQL)
target_compile_definitions(${target} PRIVATE CONF_MYSQL)
target_include_directories(${target} PRIVATE ${MYSQL_INCLUDE_DIRS})
endif()
if(TEST_MYSQL)
target_compile_definitions(${target} PRIVATE CONF_TEST_MYSQL)
endif()
if(AUTOUPDATE AND NOT STEAM)
target_compile_definitions(${target} PRIVATE CONF_AUTOUPDATE)
endif()

View file

@ -72,6 +72,16 @@ Whether to enable MySQL/MariaDB support for server. Requires at least MySQL 8.0
Note that the bundled MySQL libraries might not work properly on your system. If you run into connection problems with the MySQL server, for example that it connects as root while you chose another user, make sure to install your system libraries for the MySQL client. Make sure that the CMake configuration summary says that it found MySQL libs that were not bundled (no "using bundled libs").
* **-DTEST_MYSQL=[ON|OFF]** <br>
Whether to test MySQL/MariaDB support in GTest based tests. Note that this requires a running MySQL/MariaDB database on localhost with this setup:
```
CREATE DATABASE ddnet;
CREATE USER 'ddnet'@'localhost' IDENTIFIED BY 'thebestpassword';
GRANT ALL PRIVILEGES ON ddnet.* TO 'ddnet'@'localhost';
FLUSH PRIVILEGES;
```
* **-DAUTOUPDATE=[ON|OFF]** <br>
Whether to enable the autoupdater. Packagers may want to disable this for their packages. Default value is ON for Windows and Linux.

View file

@ -23,7 +23,7 @@ void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize)
" GameID VARCHAR(64), "
" DDNet7 BOOL DEFAULT FALSE, "
" PRIMARY KEY (Map, Name, Time, Timestamp, Server)"
");",
")",
GetPrefix(), BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate());
}
@ -39,7 +39,7 @@ void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, co
" GameID VARCHAR(64), "
" DDNet7 BOOL DEFAULT FALSE, "
" PRIMARY KEY (ID, Name)"
");",
")",
GetPrefix(), BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate(), pIdType);
}
@ -54,7 +54,7 @@ void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize)
" Stars INT DEFAULT 0, "
" Timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "
" PRIMARY KEY (Map)"
");",
")",
GetPrefix(), BinaryCollate(), BinaryCollate(), BinaryCollate());
}
@ -70,7 +70,7 @@ void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize)
" DDNet7 BOOL DEFAULT FALSE, "
" SaveID VARCHAR(36) DEFAULT NULL, "
" PRIMARY KEY (Map, Code)"
");",
")",
GetPrefix(), BinaryCollate(), BinaryCollate(), BinaryCollate());
}
@ -81,6 +81,6 @@ void IDbConnection::FormatCreatePoints(char *aBuf, unsigned int BufferSize)
" Name VARCHAR(%d) COLLATE %s NOT NULL, "
" Points INT DEFAULT 0, "
" PRIMARY KEY (Name)"
");",
")",
GetPrefix(), MAX_NAME_LENGTH, BinaryCollate());
}

View file

@ -3,6 +3,8 @@
#include <base/system.h>
#include <memory>
class IConsole;
// can hold one PreparedStatement with Results
@ -37,6 +39,8 @@ public:
virtual const char *Random() const = 0;
// Get Median Map Time from l.Map
virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const = 0;
virtual const char *False() const = 0;
virtual const char *True() const = 0;
// tries to allocate the connection from the pool established
//
@ -96,9 +100,9 @@ protected:
int MysqlInit();
void MysqlUninit();
IDbConnection *CreateSqliteConnection(const char *pFilename, bool Setup);
std::unique_ptr<IDbConnection> CreateSqliteConnection(const char *pFilename, bool Setup);
// Returns nullptr if MySQL support is not compiled in.
IDbConnection *CreateMysqlConnection(
std::unique_ptr<IDbConnection> CreateMysqlConnection(
const char *pDatabase,
const char *pPrefix,
const char *pUser,

View file

@ -1,6 +1,6 @@
#include "connection.h"
#if defined(CONF_SQL)
#if defined(CONF_MYSQL)
#include <mysql.h>
#include <base/tl/threading.h>
@ -73,6 +73,8 @@ public:
virtual const char *InsertIgnore() const { return "INSERT IGNORE"; };
virtual const char *Random() const { return "RAND()"; };
virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const;
virtual const char *False() const { return "FALSE"; }
virtual const char *True() const { return "TRUE"; }
virtual bool Connect(char *pError, int ErrorSize);
virtual void Disconnect();
@ -702,7 +704,7 @@ bool CMysqlConnection::AddPoints(const char *pPlayer, int Points, char *pError,
return false;
}
IDbConnection *CreateMysqlConnection(
std::unique_ptr<IDbConnection> CreateMysqlConnection(
const char *pDatabase,
const char *pPrefix,
const char *pUser,
@ -711,7 +713,7 @@ IDbConnection *CreateMysqlConnection(
int Port,
bool Setup)
{
return new CMysqlConnection(pDatabase, pPrefix, pUser, pPass, pIp, Port, Setup);
return std::unique_ptr<IDbConnection>(new CMysqlConnection(pDatabase, pPrefix, pUser, pPass, pIp, Port, Setup));
}
#else
int MysqlInit()
@ -721,7 +723,7 @@ int MysqlInit()
void MysqlUninit()
{
}
IDbConnection *CreateMysqlConnection(
std::unique_ptr<IDbConnection> CreateMysqlConnection(
const char *pDatabase,
const char *pPrefix,
const char *pUser,

View file

@ -23,6 +23,11 @@ public:
virtual const char *InsertIgnore() const { return "INSERT OR IGNORE"; };
virtual const char *Random() const { return "RANDOM()"; };
virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const;
// Since SQLite 3.23.0 true/false literals are recognized, but still cleaner to use 1/0, because:
// > For compatibility, if there exist columns named "true" or "false", then
// > the identifiers refer to the columns rather than Boolean constants.
virtual const char *False() const { return "0"; }
virtual const char *True() const { return "1"; }
virtual bool Connect(char *pError, int ErrorSize);
virtual void Disconnect();
@ -117,6 +122,11 @@ bool CSqliteConnection::Connect(char *pError, int ErrorSize)
return false;
}
if(sqlite3_libversion_number() < 3025000)
{
dbg_msg("sql", "SQLite version %s is not supported, use at least version 3.25.0", sqlite3_libversion());
}
int Result = sqlite3_open(m_aFilename, &m_pDb);
if(Result != SQLITE_OK)
{
@ -357,7 +367,7 @@ bool CSqliteConnection::AddPoints(const char *pPlayer, int Points, char *pError,
str_format(aBuf, sizeof(aBuf),
"INSERT INTO %s_points(Name, Points) "
"VALUES (?, ?) "
"ON CONFLICT(Name) DO UPDATE SET Points=Points+?;",
"ON CONFLICT(Name) DO UPDATE SET Points=Points+?",
GetPrefix());
if(PrepareStatement(aBuf, pError, ErrorSize))
{
@ -374,7 +384,7 @@ bool CSqliteConnection::AddPoints(const char *pPlayer, int Points, char *pError,
return false;
}
IDbConnection *CreateSqliteConnection(const char *pFilename, bool Setup)
std::unique_ptr<IDbConnection> CreateSqliteConnection(const char *pFilename, bool Setup)
{
return new CSqliteConnection(pFilename, Setup);
return std::unique_ptr<IDbConnection>(new CSqliteConnection(pFilename, Setup));
}

View file

@ -2419,17 +2419,16 @@ int CServer::Run()
if(Config()->m_SvSqliteFile[0] != '\0')
{
auto pSqlServers = std::unique_ptr<IDbConnection>(CreateSqliteConnection(
Config()->m_SvSqliteFile, true));
auto pSqliteConn = CreateSqliteConnection(Config()->m_SvSqliteFile, true);
if(Config()->m_SvUseSQL)
{
DbPool()->RegisterDatabase(std::move(pSqlServers), CDbConnectionPool::WRITE_BACKUP);
DbPool()->RegisterDatabase(std::move(pSqliteConn), CDbConnectionPool::WRITE_BACKUP);
}
else
{
auto pCopy = std::unique_ptr<IDbConnection>(pSqlServers->Copy());
DbPool()->RegisterDatabase(std::move(pSqlServers), CDbConnectionPool::READ);
auto pCopy = std::unique_ptr<IDbConnection>(pSqliteConn->Copy());
DbPool()->RegisterDatabase(std::move(pSqliteConn), CDbConnectionPool::READ);
DbPool()->RegisterDatabase(std::move(pCopy), CDbConnectionPool::WRITE);
}
}
@ -3257,12 +3256,12 @@ void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData)
bool SetUpDb = pResult->NumArguments() == 8 ? pResult->GetInteger(7) : true;
auto pSqlServers = std::unique_ptr<IDbConnection>(CreateMysqlConnection(
auto pMysqlConn = CreateMysqlConnection(
pResult->GetString(1), pResult->GetString(2), pResult->GetString(3),
pResult->GetString(4), pResult->GetString(5), pResult->GetInteger(6),
SetUpDb));
SetUpDb);
if(!pSqlServers)
if(!pMysqlConn)
{
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "can't add MySQL server: compiled without MySQL support");
return;
@ -3275,7 +3274,7 @@ void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData)
pResult->GetString(1), pResult->GetString(2), pResult->GetString(3),
pResult->GetString(5), pResult->GetInteger(6));
pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
pSelf->DbPool()->RegisterDatabase(std::move(pSqlServers), ReadOnly ? CDbConnectionPool::READ : CDbConnectionPool::WRITE);
pSelf->DbPool()->RegisterDatabase(std::move(pMysqlConn), ReadOnly ? CDbConnectionPool::READ : CDbConnectionPool::WRITE);
}
void CServer::ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData)

File diff suppressed because it is too large Load diff

View file

@ -1,311 +1,21 @@
#ifndef GAME_SERVER_SCORE_H
#define GAME_SERVER_SCORE_H
#include <atomic>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <engine/map.h>
#include <engine/server/databases/connection_pool.h>
#include <game/prng.h>
#include <game/voting.h>
#include "save.h"
#include "scoreworker.h"
struct ISqlData;
class IDbConnection;
class IServer;
class CGameContext;
enum
{
NUM_CHECKPOINTS = 25,
TIMESTAMP_STR_LENGTH = 20, // 2019-04-02 19:38:36
};
struct CScorePlayerResult : ISqlResult
{
CScorePlayerResult();
enum
{
MAX_MESSAGES = 10,
};
enum Variant
{
DIRECT,
ALL,
BROADCAST,
MAP_VOTE,
PLAYER_INFO,
} m_MessageKind;
union
{
char m_aaMessages[MAX_MESSAGES][512];
char m_aBroadcast[1024];
struct
{
float m_Time;
float m_CpTime[NUM_CHECKPOINTS];
int m_Score;
int m_HasFinishScore;
int m_Birthday; // 0 indicates no birthday
} m_Info;
struct
{
char m_aReason[VOTE_REASON_LENGTH];
char m_aServer[32 + 1];
char m_aMap[MAX_MAP_LENGTH + 1];
} m_MapVote;
} m_Data; // PLAYER_INFO
void SetVariant(Variant v);
};
struct CScoreRandomMapResult : ISqlResult
{
CScoreRandomMapResult(int ClientID) :
m_ClientID(ClientID)
{
m_aMap[0] = '\0';
m_aMessage[0] = '\0';
}
int m_ClientID;
char m_aMap[MAX_MAP_LENGTH];
char m_aMessage[512];
};
struct CScoreSaveResult : ISqlResult
{
CScoreSaveResult(int PlayerID, IGameController *Controller) :
m_Status(SAVE_FAILED),
m_SavedTeam(CSaveTeam(Controller)),
m_RequestingPlayer(PlayerID)
{
m_aMessage[0] = '\0';
m_aBroadcast[0] = '\0';
}
enum
{
SAVE_SUCCESS,
// load team in the following two cases
SAVE_FAILED,
LOAD_SUCCESS,
LOAD_FAILED,
} m_Status;
char m_aMessage[512];
char m_aBroadcast[512];
CSaveTeam m_SavedTeam;
int m_RequestingPlayer;
CUuid m_SaveID;
};
struct CScoreInitResult : ISqlResult
{
CScoreInitResult() :
m_CurrentRecord(0)
{
}
float m_CurrentRecord;
};
class CPlayerData
{
public:
CPlayerData()
{
Reset();
}
~CPlayerData() {}
void Reset()
{
m_BestTime = 0;
m_CurrentTime = 0;
for(float &BestCpTime : m_aBestCpTime)
BestCpTime = 0;
}
void Set(float Time, float CpTime[NUM_CHECKPOINTS])
{
m_BestTime = Time;
m_CurrentTime = Time;
for(int i = 0; i < NUM_CHECKPOINTS; i++)
m_aBestCpTime[i] = CpTime[i];
}
float m_BestTime;
float m_CurrentTime;
float m_aBestCpTime[NUM_CHECKPOINTS];
};
struct CSqlInitData : ISqlData
{
CSqlInitData(std::shared_ptr<CScoreInitResult> pResult) :
ISqlData(std::move(pResult))
{
}
// current map
char m_aMap[MAX_MAP_LENGTH];
};
struct CSqlPlayerRequest : ISqlData
{
CSqlPlayerRequest(std::shared_ptr<CScorePlayerResult> pResult) :
ISqlData(std::move(pResult))
{
}
// object being requested, either map (128 bytes) or player (16 bytes)
char m_aName[MAX_MAP_LENGTH];
// current map
char m_aMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
// relevant for /top5 kind of requests
int m_Offset;
char m_aServer[5];
};
struct CSqlRandomMapRequest : ISqlData
{
CSqlRandomMapRequest(std::shared_ptr<CScoreRandomMapResult> pResult) :
ISqlData(std::move(pResult))
{
}
char m_aServerType[32];
char m_aCurrentMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
int m_Stars;
};
struct CSqlScoreData : ISqlData
{
CSqlScoreData(std::shared_ptr<CScorePlayerResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlScoreData(){};
char m_aMap[MAX_MAP_LENGTH];
char m_aGameUuid[UUID_MAXSTRSIZE];
char m_aName[MAX_MAP_LENGTH];
int m_ClientID;
float m_Time;
char m_aTimestamp[TIMESTAMP_STR_LENGTH];
float m_aCpCurrent[NUM_CHECKPOINTS];
int m_Num;
bool m_Search;
char m_aRequestingPlayer[MAX_NAME_LENGTH];
};
struct CSqlTeamScoreData : ISqlData
{
CSqlTeamScoreData() :
ISqlData(nullptr)
{
}
char m_aGameUuid[UUID_MAXSTRSIZE];
char m_aMap[MAX_MAP_LENGTH];
float m_Time;
char m_aTimestamp[TIMESTAMP_STR_LENGTH];
unsigned int m_Size;
char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH];
};
struct CSqlTeamSave : ISqlData
{
CSqlTeamSave(std::shared_ptr<CScoreSaveResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlTeamSave(){};
char m_aClientName[MAX_NAME_LENGTH];
char m_aMap[MAX_MAP_LENGTH];
char m_aCode[128];
char m_aGeneratedCode[128];
char m_aServer[5];
};
struct CSqlTeamLoad : ISqlData
{
CSqlTeamLoad(std::shared_ptr<CScoreSaveResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlTeamLoad(){};
char m_aCode[128];
char m_aMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
int m_ClientID;
// struct holding all player names in the team or an empty string
char m_aClientNames[MAX_CLIENTS][MAX_NAME_LENGTH];
int m_aClientID[MAX_CLIENTS];
int m_NumPlayer;
};
struct CTeamrank
{
CUuid m_TeamID;
char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH];
unsigned int m_NumNames;
CTeamrank();
// Assumes that a database query equivalent to
//
// SELECT TeamID, Name [, ...] -- the order is important
// FROM record_teamrace
// ORDER BY TeamID, Name
//
// was executed and that the result line of the first team member is already selected.
// Afterwards the team member of the next team is selected.
//
// Returns true on SQL failure
//
// if another team can be extracted
bool NextSqlResult(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize);
bool SamePlayers(const std::vector<std::string> *aSortedNames);
};
class CScore
{
CPlayerData m_aPlayerData[MAX_CLIENTS];
CDbConnectionPool *m_pPool;
static bool Init(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool RandomMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool RandomUnfinishedMapThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool MapVoteThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool LoadPlayerDataThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool MapInfoThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTeamRankThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTopThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTeamTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowPlayerTeamTop5Thread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTimesThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTopPointsThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool GetSavesThread(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool SaveTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool LoadTeamThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool SaveScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool SaveTeamScoreThread(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
CGameContext *GameServer() const { return m_pGameServer; }
IServer *Server() const { return m_pServer; }
CGameContext *m_pGameServer;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,305 @@
#ifndef GAME_SERVER_SCOREWORKER_H
#define GAME_SERVER_SCOREWORKER_H
#include <atomic>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <engine/map.h>
#include <engine/server/databases/connection_pool.h>
#include <engine/shared/protocol.h>
#include <engine/shared/uuid_manager.h>
#include <game/server/save.h>
#include <game/voting.h>
class IDbConnection;
class IServer;
enum
{
NUM_CHECKPOINTS = 25,
TIMESTAMP_STR_LENGTH = 20, // 2019-04-02 19:38:36
};
struct CScorePlayerResult : ISqlResult
{
CScorePlayerResult();
enum
{
MAX_MESSAGES = 10,
};
enum Variant
{
DIRECT,
ALL,
BROADCAST,
MAP_VOTE,
PLAYER_INFO,
} m_MessageKind;
union
{
char m_aaMessages[MAX_MESSAGES][512];
char m_aBroadcast[1024];
struct
{
float m_Time;
float m_CpTime[NUM_CHECKPOINTS];
int m_Score;
int m_HasFinishScore;
int m_Birthday; // 0 indicates no birthday
} m_Info;
struct
{
char m_aReason[VOTE_REASON_LENGTH];
char m_aServer[32 + 1];
char m_aMap[MAX_MAP_LENGTH + 1];
} m_MapVote;
} m_Data; // PLAYER_INFO
void SetVariant(Variant v);
};
struct CScoreInitResult : ISqlResult
{
CScoreInitResult() :
m_CurrentRecord(0)
{
}
float m_CurrentRecord;
};
struct CSqlInitData : ISqlData
{
CSqlInitData(std::shared_ptr<CScoreInitResult> pResult) :
ISqlData(std::move(pResult))
{
}
// current map
char m_aMap[MAX_MAP_LENGTH];
};
struct CSqlPlayerRequest : ISqlData
{
CSqlPlayerRequest(std::shared_ptr<CScorePlayerResult> pResult) :
ISqlData(std::move(pResult))
{
}
// object being requested, either map (128 bytes) or player (16 bytes)
char m_aName[MAX_MAP_LENGTH];
// current map
char m_aMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
// relevant for /top5 kind of requests
int m_Offset;
char m_aServer[5];
};
struct CScoreRandomMapResult : ISqlResult
{
CScoreRandomMapResult(int ClientID) :
m_ClientID(ClientID)
{
m_aMap[0] = '\0';
m_aMessage[0] = '\0';
}
int m_ClientID;
char m_aMap[MAX_MAP_LENGTH];
char m_aMessage[512];
};
struct CSqlRandomMapRequest : ISqlData
{
CSqlRandomMapRequest(std::shared_ptr<CScoreRandomMapResult> pResult) :
ISqlData(std::move(pResult))
{
}
char m_aServerType[32];
char m_aCurrentMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
int m_Stars;
};
struct CSqlScoreData : ISqlData
{
CSqlScoreData(std::shared_ptr<CScorePlayerResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlScoreData(){};
char m_aMap[MAX_MAP_LENGTH];
char m_aGameUuid[UUID_MAXSTRSIZE];
char m_aName[MAX_MAP_LENGTH];
int m_ClientID;
float m_Time;
char m_aTimestamp[TIMESTAMP_STR_LENGTH];
float m_aCpCurrent[NUM_CHECKPOINTS];
int m_Num;
bool m_Search;
char m_aRequestingPlayer[MAX_NAME_LENGTH];
};
struct CScoreSaveResult : ISqlResult
{
CScoreSaveResult(int PlayerID, IGameController *Controller) :
m_Status(SAVE_FAILED),
m_SavedTeam(CSaveTeam(Controller)),
m_RequestingPlayer(PlayerID)
{
m_aMessage[0] = '\0';
m_aBroadcast[0] = '\0';
}
enum
{
SAVE_SUCCESS,
// load team in the following two cases
SAVE_FAILED,
LOAD_SUCCESS,
LOAD_FAILED,
} m_Status;
char m_aMessage[512];
char m_aBroadcast[512];
CSaveTeam m_SavedTeam;
int m_RequestingPlayer;
CUuid m_SaveID;
};
struct CSqlTeamScoreData : ISqlData
{
CSqlTeamScoreData() :
ISqlData(nullptr)
{
}
char m_aGameUuid[UUID_MAXSTRSIZE];
char m_aMap[MAX_MAP_LENGTH];
float m_Time;
char m_aTimestamp[TIMESTAMP_STR_LENGTH];
unsigned int m_Size;
char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH];
};
struct CSqlTeamSave : ISqlData
{
CSqlTeamSave(std::shared_ptr<CScoreSaveResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlTeamSave(){};
char m_aClientName[MAX_NAME_LENGTH];
char m_aMap[MAX_MAP_LENGTH];
char m_aCode[128];
char m_aGeneratedCode[128];
char m_aServer[5];
};
struct CSqlTeamLoad : ISqlData
{
CSqlTeamLoad(std::shared_ptr<CScoreSaveResult> pResult) :
ISqlData(std::move(pResult))
{
}
virtual ~CSqlTeamLoad(){};
char m_aCode[128];
char m_aMap[MAX_MAP_LENGTH];
char m_aRequestingPlayer[MAX_NAME_LENGTH];
int m_ClientID;
// struct holding all player names in the team or an empty string
char m_aClientNames[MAX_CLIENTS][MAX_NAME_LENGTH];
int m_aClientID[MAX_CLIENTS];
int m_NumPlayer;
};
class CPlayerData
{
public:
CPlayerData()
{
Reset();
}
~CPlayerData() {}
void Reset()
{
m_BestTime = 0;
m_CurrentTime = 0;
for(float &BestCpTime : m_aBestCpTime)
BestCpTime = 0;
}
void Set(float Time, float CpTime[NUM_CHECKPOINTS])
{
m_BestTime = Time;
m_CurrentTime = Time;
for(int i = 0; i < NUM_CHECKPOINTS; i++)
m_aBestCpTime[i] = CpTime[i];
}
float m_BestTime;
float m_CurrentTime;
float m_aBestCpTime[NUM_CHECKPOINTS];
};
struct CTeamrank
{
CUuid m_TeamID;
char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH];
unsigned int m_NumNames;
CTeamrank();
// Assumes that a database query equivalent to
//
// SELECT TeamID, Name [, ...] -- the order is important
// FROM record_teamrace
// ORDER BY TeamID, Name
//
// was executed and that the result line of the first team member is already selected.
// Afterwards the team member of the next team is selected.
//
// Returns true on SQL failure
//
// if another team can be extracted
bool NextSqlResult(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize);
bool SamePlayers(const std::vector<std::string> *aSortedNames);
};
struct CScoreWorker
{
static bool Init(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool RandomMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool RandomUnfinishedMap(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool MapVote(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool LoadPlayerData(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool MapInfo(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTop(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTimes(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool ShowTopPoints(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool GetSaves(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize);
static bool SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool SaveScore(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
static bool SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGameData, bool Failure, char *pError, int ErrorSize);
};
#endif // GAME_SERVER_SCOREWORKER_H

488
src/test/score.cpp Normal file
View file

@ -0,0 +1,488 @@
#include <gtest/gtest.h>
#include <base/detect.h>
#include <engine/server/databases/connection.h>
#include <engine/shared/config.h>
#include <game/server/scoreworker.h>
#include <sqlite3.h>
#if defined(CONF_TEST_MYSQL)
int DummyMysqlInit = (MysqlInit(), 1);
#endif
char *CSaveTeam::GetString()
{
// Dummy implementation for testing
return nullptr;
}
int CSaveTeam::FromString(char const *)
{
// Dummy implementation for testing
return 1;
}
bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen)
{
// Dummy implementation for testing
return false;
}
TEST(SQLite, Version)
{
ASSERT_GE(sqlite3_libversion_number(), 3025000) << "SQLite >= 3.25.0 required for Window functions";
}
struct Score : public testing::TestWithParam<IDbConnection *>
{
Score()
{
Connect();
Init();
InsertMap();
}
~Score()
{
conn->Disconnect();
}
void Connect()
{
ASSERT_FALSE(conn->Connect(aError, sizeof(aError))) << aError;
// Delete all existing entries for persistent databases like MySQL
int NumInserted = 0;
ASSERT_FALSE(conn->PrepareStatement("DELETE FROM record_race", aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->PrepareStatement("DELETE FROM record_teamrace", aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->PrepareStatement("DELETE FROM record_maps", aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->PrepareStatement("DELETE FROM record_points", aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->PrepareStatement("DELETE FROM record_saves", aError, sizeof(aError))) << aError;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
}
void Init()
{
CSqlInitData initData(std::make_shared<CScoreInitResult>());
str_copy(initData.m_aMap, "Kobra 3", sizeof(initData.m_aMap));
ASSERT_FALSE(CScoreWorker::Init(conn, &initData, aError, sizeof(aError))) << aError;
}
void InsertMap()
{
char aBuf[512];
str_format(aBuf, sizeof(aBuf),
"%s into %s_maps(Map, Server, Mapper, Points, Stars, Timestamp) "
"VALUES (\"Kobra 3\", \"Novice\", \"Zerodin\", 5, 5, \"2015-01-01 00:00:00\")",
conn->InsertIgnore(), conn->GetPrefix());
ASSERT_FALSE(conn->PrepareStatement(aBuf, aError, sizeof(aError))) << aError;
int NumInserted = 0;
ASSERT_FALSE(conn->ExecuteUpdate(&NumInserted, aError, sizeof(aError))) << aError;
ASSERT_EQ(NumInserted, 1);
}
void InsertRank()
{
str_copy(g_Config.m_SvSqlServerName, "USA", sizeof(g_Config.m_SvSqlServerName));
CSqlScoreData scoreData(std::make_shared<CScorePlayerResult>());
str_copy(scoreData.m_aMap, "Kobra 3", sizeof(scoreData.m_aMap));
str_copy(scoreData.m_aGameUuid, "8d300ecf-5873-4297-bee5-95668fdff320", sizeof(scoreData.m_aGameUuid));
str_copy(scoreData.m_aName, "nameless tee", sizeof(scoreData.m_aName));
scoreData.m_ClientID = 0;
scoreData.m_Time = 100.0;
str_copy(scoreData.m_aTimestamp, "2021-11-24 19:24:08", sizeof(scoreData.m_aTimestamp));
for(int i = 0; i < NUM_CHECKPOINTS; i++)
scoreData.m_aCpCurrent[i] = i;
str_copy(scoreData.m_aRequestingPlayer, "deen", sizeof(scoreData.m_aRequestingPlayer));
ASSERT_FALSE(CScoreWorker::SaveScore(conn, &scoreData, false, aError, sizeof(aError))) << aError;
}
void ExpectLines(std::shared_ptr<CScorePlayerResult> pPlayerResult, std::initializer_list<const char *> Lines, bool All = false)
{
EXPECT_EQ(pPlayerResult->m_MessageKind, All ? CScorePlayerResult::ALL : CScorePlayerResult::DIRECT);
int i = 0;
for(const char *pLine : Lines)
{
EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[i], pLine);
i++;
}
for(; i < CScorePlayerResult::MAX_MESSAGES; i++)
{
EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[i], "");
}
}
IDbConnection *conn{GetParam()};
char aError[256] = {};
std::shared_ptr<CScorePlayerResult> pPlayerResult{std::make_shared<CScorePlayerResult>()};
CSqlPlayerRequest playerRequest{pPlayerResult};
};
struct SingleScore : public Score
{
SingleScore()
{
InsertRank();
str_copy(playerRequest.m_aMap, "Kobra 3", sizeof(playerRequest.m_aMap));
str_copy(playerRequest.m_aRequestingPlayer, "brainless tee", sizeof(playerRequest.m_aRequestingPlayer));
playerRequest.m_Offset = 0;
str_copy(playerRequest.m_aServer, "GER", sizeof(playerRequest.m_aServer));
str_copy(playerRequest.m_aName, "nameless tee", sizeof(playerRequest.m_aMap));
}
};
TEST_P(SingleScore, Top)
{
ASSERT_FALSE(CScoreWorker::ShowTop(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"------------ Global Top ------------",
"1. nameless tee Time: 01:40.00",
"------------ GER Top ------------"});
}
TEST_P(SingleScore, Rank)
{
ASSERT_FALSE(CScoreWorker::ShowRank(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1 - GER unranked"}, true);
}
TEST_P(SingleScore, TopServer)
{
str_copy(playerRequest.m_aServer, "USA", sizeof(playerRequest.m_aServer));
ASSERT_FALSE(CScoreWorker::ShowTop(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"------------ Global Top ------------",
"1. nameless tee Time: 01:40.00",
"---------------------------------------"});
}
TEST_P(SingleScore, RankServer)
{
str_copy(playerRequest.m_aServer, "USA", sizeof(playerRequest.m_aServer));
ASSERT_FALSE(CScoreWorker::ShowRank(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"nameless tee - 01:40.00 - better than 100% - requested by brainless tee", "Global rank 1 - USA rank 1"}, true);
}
TEST_P(SingleScore, TimesExists)
{
ASSERT_FALSE(CScoreWorker::ShowTimes(conn, &playerRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pPlayerResult->m_MessageKind, CScorePlayerResult::DIRECT);
EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[0], "------------- Last Times -------------");
char aBuf[128];
str_copy(aBuf, pPlayerResult->m_Data.m_aaMessages[1], 7);
EXPECT_STREQ(aBuf, "[USA] ");
str_copy(aBuf, pPlayerResult->m_Data.m_aaMessages[1] + str_length(pPlayerResult->m_Data.m_aaMessages[1]) - 10, 11);
EXPECT_STREQ(aBuf, ", 01:40.00");
EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[2], "----------------------------------------------------");
for(int i = 3; i < CScorePlayerResult::MAX_MESSAGES; i++)
{
EXPECT_STREQ(pPlayerResult->m_Data.m_aaMessages[i], "");
}
}
TEST_P(SingleScore, TimesDoesntExist)
{
str_copy(playerRequest.m_aName, "foo", sizeof(playerRequest.m_aMap));
ASSERT_FALSE(CScoreWorker::ShowTimes(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"There are no times in the specified range"});
}
struct TeamScore : public Score
{
void SetUp()
{
CSqlTeamScoreData teamScoreData;
str_copy(teamScoreData.m_aMap, "Kobra 3", sizeof(teamScoreData.m_aMap));
str_copy(teamScoreData.m_aGameUuid, "8d300ecf-5873-4297-bee5-95668fdff320", sizeof(teamScoreData.m_aGameUuid));
teamScoreData.m_Size = 2;
str_copy(teamScoreData.m_aaNames[0], "nameless tee", sizeof(teamScoreData.m_aaNames[0]));
str_copy(teamScoreData.m_aaNames[1], "brainless tee", sizeof(teamScoreData.m_aaNames[1]));
teamScoreData.m_Time = 100.0;
str_copy(teamScoreData.m_aTimestamp, "2021-11-24 19:24:08", sizeof(teamScoreData.m_aTimestamp));
ASSERT_FALSE(CScoreWorker::SaveTeamScore(conn, &teamScoreData, false, aError, sizeof(aError))) << aError;
str_copy(playerRequest.m_aMap, "Kobra 3", sizeof(playerRequest.m_aMap));
str_copy(playerRequest.m_aRequestingPlayer, "brainless tee", sizeof(playerRequest.m_aRequestingPlayer));
playerRequest.m_Offset = 0;
}
};
TEST_P(TeamScore, All)
{
ASSERT_FALSE(CScoreWorker::ShowTeamTop5(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"------- Team Top 5 -------",
"1. brainless tee & nameless tee Team Time: 01:40.00",
"-------------------------------"});
}
TEST_P(TeamScore, PlayerExists)
{
str_copy(playerRequest.m_aName, "brainless tee", sizeof(playerRequest.m_aMap));
ASSERT_FALSE(CScoreWorker::ShowPlayerTeamTop5(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"------- Team Top 5 -------",
"1. brainless tee & nameless tee Team Time: 01:40.00",
"-------------------------------"});
}
TEST_P(TeamScore, PlayerDoesntExist)
{
str_copy(playerRequest.m_aName, "foo", sizeof(playerRequest.m_aMap));
ASSERT_FALSE(CScoreWorker::ShowPlayerTeamTop5(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"foo has no team ranks"});
}
struct MapInfo : public Score
{
MapInfo()
{
str_copy(playerRequest.m_aRequestingPlayer, "brainless tee", sizeof(playerRequest.m_aRequestingPlayer));
}
};
TEST_P(MapInfo, ExactNoFinish)
{
str_copy(playerRequest.m_aName, "Kobra 3", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapInfo(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released 6 years and 11 months ago, 0 finishes by 0 tees"});
}
TEST_P(MapInfo, ExactFinish)
{
InsertRank();
str_copy(playerRequest.m_aName, "Kobra 3", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapInfo(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released 6 years and 11 months ago, 1 finish by 1 tee in 01:40 median"});
}
TEST_P(MapInfo, Fuzzy)
{
InsertRank();
str_copy(playerRequest.m_aName, "k3", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapInfo(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"\"Kobra 3\" by Zerodin on Novice, ★★★★★, 5 points, released 6 years and 11 months ago, 1 finish by 1 tee in 01:40 median"});
}
TEST_P(MapInfo, DoesntExit)
{
str_copy(playerRequest.m_aName, "f", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapInfo(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"No map like \"f\" found."});
}
struct MapVote : public Score
{
MapVote()
{
str_copy(playerRequest.m_aRequestingPlayer, "brainless tee", sizeof(playerRequest.m_aRequestingPlayer));
}
};
TEST_P(MapVote, Exact)
{
str_copy(playerRequest.m_aName, "Kobra 3", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapVote(conn, &playerRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
}
TEST_P(MapVote, Fuzzy)
{
str_copy(playerRequest.m_aName, "k3", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapVote(conn, &playerRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pPlayerResult->m_MessageKind, CScorePlayerResult::MAP_VOTE);
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aMap, "Kobra 3");
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aReason, "/map");
EXPECT_STREQ(pPlayerResult->m_Data.m_MapVote.m_aServer, "novice");
}
TEST_P(MapVote, DoesntExist)
{
str_copy(playerRequest.m_aName, "f", sizeof(playerRequest.m_aName));
ASSERT_FALSE(CScoreWorker::MapVote(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"No map like \"f\" found. Try adding a '%' at the start if you don't know the first character. Example: /map %castle for \"Out of Castle\""});
}
struct Points : public Score
{
Points()
{
str_copy(playerRequest.m_aName, "nameless tee", sizeof(playerRequest.m_aName));
str_copy(playerRequest.m_aRequestingPlayer, "brainless tee", sizeof(playerRequest.m_aRequestingPlayer));
playerRequest.m_Offset = 0;
}
};
TEST_P(Points, NoPoints)
{
ASSERT_FALSE(CScoreWorker::ShowPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"nameless tee has not collected any points so far"});
}
TEST_P(Points, NoPointsTop)
{
ASSERT_FALSE(CScoreWorker::ShowTopPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"-------- Top Points --------",
"-------------------------------"});
}
TEST_P(Points, OnePoints)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"1. nameless tee Points: 2, requested by brainless tee"}, true);
}
TEST_P(Points, OnePointsTop)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowTopPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"-------- Top Points --------",
"1. nameless tee Points: 2",
"-------------------------------"});
}
TEST_P(Points, TwoPoints)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
conn->AddPoints("brainless tee", 3, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"2. nameless tee Points: 2, requested by brainless tee"}, true);
}
TEST_P(Points, TwoPointsTop)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
conn->AddPoints("brainless tee", 3, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowTopPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"-------- Top Points --------",
"1. brainless tee Points: 3",
"2. nameless tee Points: 2",
"-------------------------------"});
}
TEST_P(Points, EqualPoints)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
conn->AddPoints("brainless tee", 3, aError, sizeof(aError));
conn->AddPoints("nameless tee", 1, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult, {"1. nameless tee Points: 3, requested by brainless tee"}, true);
}
TEST_P(Points, EqualPointsTop)
{
conn->AddPoints("nameless tee", 2, aError, sizeof(aError));
conn->AddPoints("brainless tee", 3, aError, sizeof(aError));
conn->AddPoints("nameless tee", 1, aError, sizeof(aError));
ASSERT_FALSE(CScoreWorker::ShowTopPoints(conn, &playerRequest, aError, sizeof(aError))) << aError;
ExpectLines(pPlayerResult,
{"-------- Top Points --------",
"1. brainless tee Points: 3",
"1. nameless tee Points: 3",
"-------------------------------"});
}
struct RandomMap : public Score
{
std::shared_ptr<CScoreRandomMapResult> pRandomMapResult{std::make_shared<CScoreRandomMapResult>(0)};
CSqlRandomMapRequest randomMapRequest{pRandomMapResult};
RandomMap()
{
str_copy(randomMapRequest.m_aServerType, "Novice", sizeof(randomMapRequest.m_aServerType));
str_copy(randomMapRequest.m_aCurrentMap, "Kobra 4", sizeof(randomMapRequest.m_aCurrentMap));
str_copy(randomMapRequest.m_aRequestingPlayer, "nameless tee", sizeof(randomMapRequest.m_aRequestingPlayer));
}
};
TEST_P(RandomMap, NoStars)
{
randomMapRequest.m_Stars = -1;
ASSERT_FALSE(CScoreWorker::RandomMap(conn, &randomMapRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pRandomMapResult->m_ClientID, 0);
EXPECT_STREQ(pRandomMapResult->m_aMap, "Kobra 3");
EXPECT_STREQ(pRandomMapResult->m_aMessage, "");
}
TEST_P(RandomMap, StarsExists)
{
randomMapRequest.m_Stars = 5;
ASSERT_FALSE(CScoreWorker::RandomMap(conn, &randomMapRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pRandomMapResult->m_ClientID, 0);
EXPECT_STREQ(pRandomMapResult->m_aMap, "Kobra 3");
EXPECT_STREQ(pRandomMapResult->m_aMessage, "");
}
TEST_P(RandomMap, StarsDoesntExist)
{
randomMapRequest.m_Stars = 3;
ASSERT_FALSE(CScoreWorker::RandomMap(conn, &randomMapRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pRandomMapResult->m_ClientID, 0);
EXPECT_STREQ(pRandomMapResult->m_aMap, "");
EXPECT_STREQ(pRandomMapResult->m_aMessage, "No maps found on this server!");
}
TEST_P(RandomMap, UnfinishedExists)
{
randomMapRequest.m_Stars = -1;
ASSERT_FALSE(CScoreWorker::RandomUnfinishedMap(conn, &randomMapRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pRandomMapResult->m_ClientID, 0);
EXPECT_STREQ(pRandomMapResult->m_aMap, "Kobra 3");
EXPECT_STREQ(pRandomMapResult->m_aMessage, "");
}
TEST_P(RandomMap, UnfinishedDoesntExist)
{
InsertRank();
ASSERT_FALSE(CScoreWorker::RandomUnfinishedMap(conn, &randomMapRequest, aError, sizeof(aError))) << aError;
EXPECT_EQ(pRandomMapResult->m_ClientID, 0);
EXPECT_STREQ(pRandomMapResult->m_aMap, "");
EXPECT_STREQ(pRandomMapResult->m_aMessage, "You have no more unfinished maps on this server!");
}
auto pSqliteConn = CreateSqliteConnection(":memory:", true);
#if defined(CONF_TEST_MYSQL)
auto pMysqlConn = CreateMysqlConnection("ddnet", "record", "ddnet", "thebestpassword", "localhost", 3306, true);
#endif
auto testValues
{
testing::Values(pSqliteConn.get()
#if defined(CONF_TEST_MYSQL)
,
pMysqlConn.get()
#endif
)
};
#define INSTANTIATE(SUITE) \
INSTANTIATE_TEST_SUITE_P(Sql, SUITE, testValues, \
[](const testing::TestParamInfo<Score::ParamType> &info) { \
switch(info.index) \
{ \
case 0: return "SQLite"; \
case 1: return "MySQL"; \
default: return "Unknown"; \
} \
})
INSTANTIATE(SingleScore);
INSTANTIATE(TeamScore);
INSTANTIATE(MapInfo);
INSTANTIATE(MapVote);
INSTANTIATE(Points);
INSTANTIATE(RandomMap);