Test: Add upstream tests

wip-string
Jonathan G Rennison 5 months ago
parent 24c8a8f887
commit 34668bff87

@ -23,6 +23,7 @@ void UpdateFontHeightCache();
/** Font cache for basic fonts. */
class FontCache {
friend class MockFontCache;
private:
static FontCache *caches[FS_END]; ///< All the font caches.
protected:

@ -11,3 +11,7 @@ add_files(
survey_osx.cpp
CONDITION APPLE
)
if(APPLE)
target_sources(openttd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/osx_main.cpp)
endif()

@ -0,0 +1,51 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file unix_main.cpp Main entry for Mac OSX. */
#include "../../stdafx.h"
#include "../../openttd.h"
#include "../../crashlog.h"
#include "../../core/random_func.hpp"
#include "../../string_func.h"
#include "../../thread.h"
#include <time.h>
#include <signal.h>
#include "macos.h"
#include "../../safeguards.h"
void CocoaSetupAutoreleasePool();
void CocoaReleaseAutoreleasePool();
int CDECL main(int argc, char *argv[])
{
/* Make sure our arguments contain only valid UTF-8 characters. */
for (int i = 0; i < argc; i++) StrMakeValidInPlace(argv[i]);
CocoaSetupAutoreleasePool();
/* This is passed if we are launched by double-clicking */
if (argc >= 2 && strncmp(argv[1], "-psn", 4) == 0) {
argv[1] = nullptr;
argc = 1;
}
PerThreadSetupInit();
CrashLog::InitialiseCrashLog();
SetRandomSeed(time(nullptr));
signal(SIGPIPE, SIG_IGN);
int ret = openttd_main(argc, argv);
CocoaReleaseAutoreleasePool();
return ret;
}

@ -13,3 +13,7 @@ add_files(
font_unix.cpp
CONDITION Fontconfig_FOUND
)
if(UNIX AND NOT APPLE)
target_sources(openttd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/unix_main.cpp)
endif()

@ -52,11 +52,6 @@
#endif
#if defined(__APPLE__)
# if defined(WITH_SDL)
/* the mac implementation needs this file included in the same file as main() */
# include <SDL.h>
# endif
# include "../macosx/macos.h"
#endif
@ -229,40 +224,6 @@ void NORETURN DoOSAbort()
}
#endif
#ifdef WITH_COCOA
void CocoaSetupAutoreleasePool();
void CocoaReleaseAutoreleasePool();
#endif
int CDECL main(int argc, char *argv[])
{
/* Make sure our arguments contain only valid UTF-8 characters. */
for (int i = 0; i < argc; i++) StrMakeValidInPlace(argv[i]);
#ifdef WITH_COCOA
CocoaSetupAutoreleasePool();
/* This is passed if we are launched by double-clicking */
if (argc >= 2 && strncmp(argv[1], "-psn", 4) == 0) {
argv[1] = nullptr;
argc = 1;
}
#endif
PerThreadSetupInit();
CrashLog::InitialiseCrashLog();
SetRandomSeed(time(nullptr));
signal(SIGPIPE, SIG_IGN);
int ret = openttd_main(argc, argv);
#ifdef WITH_COCOA
CocoaReleaseAutoreleasePool();
#endif
return ret;
}
#ifndef WITH_COCOA
std::optional<std::string> GetClipboardContents()
{

@ -0,0 +1,35 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file unix_main.cpp Main entry for Unix. */
#include "../../stdafx.h"
#include "../../openttd.h"
#include "../../crashlog.h"
#include "../../core/random_func.hpp"
#include "../../string_func.h"
#include "../../thread.h"
#include <time.h>
#include <signal.h>
#include "../../safeguards.h"
int CDECL main(int argc, char *argv[])
{
/* Make sure our arguments contain only valid UTF-8 characters. */
for (int i = 0; i < argc; i++) StrMakeValidInPlace(argv[i]);
PerThreadSetupInit();
CrashLog::InitialiseCrashLog();
SetRandomSeed(time(nullptr));
signal(SIGPIPE, SIG_IGN);
return openttd_main(argc, argv);
}

@ -9,3 +9,7 @@ add_files(
win32.h
CONDITION WIN32
)
if(WIN32)
target_sources(openttd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/win32_main.cpp)
endif()

@ -270,37 +270,6 @@ std::optional<uint64_t> FiosGetDiskFreeSpace(const std::string &path)
return std::nullopt;
}
static int ParseCommandLine(char *line, char **argv, int max_argc)
{
int n = 0;
do {
/* skip whitespace */
while (*line == ' ' || *line == '\t') line++;
/* end? */
if (*line == '\0') break;
/* special handling when quoted */
if (*line == '"') {
argv[n++] = ++line;
while (*line != '"') {
if (*line == '\0') return n;
line++;
}
} else {
argv[n++] = line;
while (*line != ' ' && *line != '\t') {
if (*line == '\0') return n;
line++;
}
}
*line++ = '\0';
} while (n != max_argc);
return n;
}
void CreateConsole()
{
HANDLE hand;
@ -419,45 +388,6 @@ void ShowInfo(const char *str)
}
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
int argc;
char *argv[64]; // max 64 command line arguments
/* Set system timer resolution to 1ms. */
timeBeginPeriod(1);
PerThreadSetupInit();
CrashLog::InitialiseCrashLog();
/* Convert the command line to UTF-8. */
std::string cmdline = FS2OTTD(GetCommandLine());
/* Set the console codepage to UTF-8. */
SetConsoleOutputCP(CP_UTF8);
#if defined(_DEBUG)
CreateConsole();
#endif
_set_error_mode(_OUT_TO_MSGBOX); // force assertion output to messagebox
/* setup random seed to something quite random */
SetRandomSeed(GetTickCount());
argc = ParseCommandLine(cmdline.data(), argv, lengthof(argv));
/* Make sure our arguments contain only valid UTF-8 characters. */
for (int i = 0; i < argc; i++) StrMakeValidInPlace(argv[i]);
openttd_main(argc, argv);
/* Restore system timer resolution. */
timeEndPeriod(1);
return 0;
}
char *getcwd(char *buf, size_t size)
{
wchar_t path[MAX_PATH];

@ -0,0 +1,92 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file win32_main.cpp Implementation main for Windows. */
#include "../../stdafx.h"
#include <windows.h>
#include <mmsystem.h>
#include "../../openttd.h"
#include "../../core/random_func.hpp"
#include "../../string_func.h"
#include "../../crashlog.h"
#include "../../debug.h"
#include "../../thread.h"
#include "../../safeguards.h"
static int ParseCommandLine(char *line, char **argv, int max_argc)
{
int n = 0;
do {
/* skip whitespace */
while (*line == ' ' || *line == '\t') line++;
/* end? */
if (*line == '\0') break;
/* special handling when quoted */
if (*line == '"') {
argv[n++] = ++line;
while (*line != '"') {
if (*line == '\0') return n;
line++;
}
} else {
argv[n++] = line;
while (*line != ' ' && *line != '\t') {
if (*line == '\0') return n;
line++;
}
}
*line++ = '\0';
} while (n != max_argc);
return n;
}
void CreateConsole();
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
int argc;
char *argv[64]; // max 64 command line arguments
/* Set system timer resolution to 1ms. */
timeBeginPeriod(1);
PerThreadSetupInit();
CrashLog::InitialiseCrashLog();
/* Convert the command line to UTF-8. */
std::string cmdline = FS2OTTD(GetCommandLine());
/* Set the console codepage to UTF-8. */
SetConsoleOutputCP(CP_UTF8);
#if defined(_DEBUG)
CreateConsole();
#endif
_set_error_mode(_OUT_TO_MSGBOX); // force assertion output to messagebox
/* setup random seed to something quite random */
SetRandomSeed(GetTickCount());
argc = ParseCommandLine(cmdline.data(), argv, lengthof(argv));
/* Make sure our arguments contain only valid UTF-8 characters. */
for (int i = 0; i < argc; i++) StrMakeValidInPlace(argv[i]);
openttd_main(argc, argv);
/* Restore system timer resolution. */
timeEndPeriod(1);
return 0;
}

@ -200,7 +200,7 @@
#endif /* defined(_MSC_VER) */
#if !defined(STRGEN) && !defined(SETTINGSGEN) && !defined(OPENTTD_TEST)
#if !defined(STRGEN) && !defined(SETTINGSGEN)
# if defined(_WIN32)
char *getcwd(char *buf, size_t size);
# include <io.h>
@ -220,7 +220,7 @@
template <typename T> std::string FS2OTTD(T name) { return name; }
template <typename T> std::string OTTD2FS(T name) { return name; }
# endif /* _WIN32 or WITH_ICONV */
#endif /* STRGEN || SETTINGSGEN || OPENTTD_TEST */
#endif /* STRGEN || SETTINGSGEN */
#if defined(_WIN32)
# define PATHSEP "\\"

@ -2,10 +2,14 @@ add_test_files(
bitmath_func.cpp
landscape_partial_pixel_z.cpp
math_func.cpp
mock_environment.h
mock_fontcache.h
mock_spritecache.cpp
mock_spritecache.h
ring_buffer.cpp
string_func.cpp
strings_func.cpp
test_main.cpp
../landscape_ppz.cpp
../core/alloc_func.cpp
../core/bitmath_func.cpp
../core/math_func.cpp
test_script_admin.cpp
test_window_desc.cpp
)

@ -7,7 +7,6 @@
/** @file bitmath_func_test.cpp Test functionality from core/bitmath_func. */
#define OPENTTD_TEST
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"

@ -7,7 +7,6 @@
/** @file landscape_partial_pixel_z.cpp Tests for consistency/validity of the results of GetPartialPixelZ. */
#define OPENTTD_TEST
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"

@ -5,9 +5,8 @@
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file math_func_test.cpp Test functionality from core/math_func. */
/** @file math_func.cpp Test functionality from core/math_func. */
#define OPENTTD_TEST
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
@ -100,10 +99,10 @@ TEST_CASE("ClampTo")
CHECK(127 == ClampTo<int8_t>(127));
CHECK(126 == ClampTo<int8_t>(126));
CHECK(126 == ClampTo<int64_t>(static_cast<uint8>(126)));
CHECK(126 == ClampTo<uint64_t>(static_cast<int8>(126)));
CHECK(0 == ClampTo<uint64_t>(static_cast<int8>(-126)));
CHECK(0 == ClampTo<uint8_t>(static_cast<int8>(-126)));
CHECK(126 == ClampTo<int64_t>(static_cast<uint8_t>(126)));
CHECK(126 == ClampTo<uint64_t>(static_cast<int8_t>(126)));
CHECK(0 == ClampTo<uint64_t>(static_cast<int8_t>(-126)));
CHECK(0 == ClampTo<uint8_t>(static_cast<int8_t>(-126)));
/* The realm around 64 bits types is tricky as there is not one type/method that works for all. */

@ -0,0 +1,39 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file mock_environment.h Singleton instance to create a mock FontCache/SpriteCache environment. */
#ifndef MOCK_ENVIRONMENT_H
#define MOCK_ENVIRONMENT_H
#include "mock_fontcache.h"
#include "mock_spritecache.h"
/** Singleton class to set up the mock environemnt once. */
class MockEnvironment {
public:
static MockEnvironment &Instance()
{
static MockEnvironment instance;
return instance;
}
MockEnvironment(MockEnvironment const &) = delete;
void operator=(MockEnvironment const &) = delete;
private:
MockEnvironment()
{
/* Mock SpriteCache initialization is needed for some widget generators. */
MockGfxLoadSprites();
/* Mock FontCache initialization is needed for some NWidgetParts. */
MockFontCache::InitializeFontCaches();
}
};
#endif /* MOCK_ENVIRONMENT_H */

@ -0,0 +1,45 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file mock_fontcache.h Mock font cache implementation definition. */
#ifndef MOCK_FONTCACHE_H
#define MOCK_FONTCACHE_H
#include "../stdafx.h"
#include "../fontcache.h"
#include "../string_func.h"
/** Font cache for mocking basic use of fonts. */
class MockFontCache : public FontCache {
public:
MockFontCache(FontSize fs) : FontCache(fs)
{
this->height = FontCache::GetDefaultFontHeight(this->fs);
}
void SetUnicodeGlyph(char32_t, SpriteID) override {}
void InitializeUnicodeGlyphMap() override {}
void ClearFontCache() override {}
const Sprite *GetGlyph(GlyphID) override { return nullptr; }
uint GetGlyphWidth(GlyphID) override { return this->height / 2; }
bool GetDrawGlyphShadow() override { return false; }
GlyphID MapCharToGlyph(char32_t key) override { return key; }
const void *GetFontTable(uint32_t, size_t &length) override { length = 0; return nullptr; }
std::string GetFontName() override { return "mock"; }
bool IsBuiltInFont() override { return true; }
static void InitializeFontCaches()
{
for (FontSize fs = FS_BEGIN; fs != FS_END; fs++) {
if (FontCache::caches[fs] == nullptr) new MockFontCache(fs); /* FontCache inserts itself into to the cache. */
}
}
};
#endif /* MOCK_FONTCACHE_H */

@ -0,0 +1,49 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file mock_spritecache.cpp Mock sprite cache implementation. */
#include "../stdafx.h"
#include "../blitter/factory.hpp"
#include "../core/math_func.hpp"
#include "../spritecache.h"
#include "../spritecache_internal.h"
#include "../table/sprites.h"
static bool MockLoadNextSprite(int load_index)
{
SpriteDataBuffer buffer;
buffer.Allocate((uint32)sizeof(Sprite));
memset(buffer.GetPtr(), 0, buffer.GetSize());
bool is_mapgen = IsMapgenSpriteID(load_index);
SpriteCache *sc = AllocateSpriteCache(load_index);
sc->file = nullptr;
sc->file_pos = 0;
sc->Assign(std::move(buffer));
sc->id = 0;
sc->type = is_mapgen ? SpriteType::MapGen : SpriteType::Normal;
sc->flags = 0;
/* Fill with empty sprites up until the default sprite count. */
return (uint)load_index < SPR_OPENTTD_BASE + OPENTTD_SPRITE_COUNT;
}
void MockGfxLoadSprites()
{
/* Force blitter 'null'. This is necessary for GfxInitSpriteMem() to function. */
BlitterFactory::SelectBlitter("null");
GfxInitSpriteMem();
int load_index = 0;
while (MockLoadNextSprite(load_index)) {
load_index++;
}
}

@ -0,0 +1,15 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file mock_spritecache.h Mock sprite cache definition. */
#ifndef MOCK_SPRITECACHE_H
#define MOCK_SPRITECACHE_H
void MockGfxLoadSprites();
#endif /* MOCK_SPRITECACHE_H */

@ -7,7 +7,6 @@
/** @file ring_buffer.cpp Test functionality from core/ring_buffer.hpp */
#define OPENTTD_TEST
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"

@ -0,0 +1,527 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file string_func.cpp Test functionality from string_func. */
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
#include "../string_func.h"
#include <array>
/**** String compare/equals *****/
TEST_CASE("StrCompareIgnoreCase - std::string")
{
/* Same string, with different cases. */
CHECK(StrCompareIgnoreCase(std::string{""}, std::string{""}) == 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"a"}) == 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"A"}) == 0);
CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"a"}) == 0);
CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"A"}) == 0);
/* Not the same string. */
CHECK(StrCompareIgnoreCase(std::string{""}, std::string{"b"}) < 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{""}) > 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"b"}) < 0);
CHECK(StrCompareIgnoreCase(std::string{"b"}, std::string{"a"}) > 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"B"}) < 0);
CHECK(StrCompareIgnoreCase(std::string{"b"}, std::string{"A"}) > 0);
CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"b"}) < 0);
CHECK(StrCompareIgnoreCase(std::string{"B"}, std::string{"a"}) > 0);
CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"aa"}) < 0);
CHECK(StrCompareIgnoreCase(std::string{"aa"}, std::string{"a"}) > 0);
}
TEST_CASE("StrCompareIgnoreCase - char pointer")
{
/* Same string, with different cases. */
CHECK(StrCompareIgnoreCase("", "") == 0);
CHECK(StrCompareIgnoreCase("a", "a") == 0);
CHECK(StrCompareIgnoreCase("a", "A") == 0);
CHECK(StrCompareIgnoreCase("A", "a") == 0);
CHECK(StrCompareIgnoreCase("A", "A") == 0);
/* Not the same string. */
CHECK(StrCompareIgnoreCase("", "b") < 0);
CHECK(StrCompareIgnoreCase("a", "") > 0);
CHECK(StrCompareIgnoreCase("a", "b") < 0);
CHECK(StrCompareIgnoreCase("b", "a") > 0);
CHECK(StrCompareIgnoreCase("a", "B") < 0);
CHECK(StrCompareIgnoreCase("b", "A") > 0);
CHECK(StrCompareIgnoreCase("A", "b") < 0);
CHECK(StrCompareIgnoreCase("B", "a") > 0);
CHECK(StrCompareIgnoreCase("a", "aa") < 0);
CHECK(StrCompareIgnoreCase("aa", "a") > 0);
}
TEST_CASE("StrCompareIgnoreCase - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aaAbB"};
/* Same string, with different cases. */
CHECK(StrCompareIgnoreCase(base.substr(0, 0), base.substr(1, 0)) == 0); // Different positions
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(1, 1)) == 0); // Different positions
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(2, 1)) == 0);
CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(1, 1)) == 0);
CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(2, 1)) == 0);
/* Not the same string. */
CHECK(StrCompareIgnoreCase(base.substr(3, 0), base.substr(3, 1)) < 0); // Same position, different lengths
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(0, 0)) > 0); // Same position, different lengths
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(3, 1)) < 0);
CHECK(StrCompareIgnoreCase(base.substr(3, 1), base.substr(0, 1)) > 0);
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(4, 1)) < 0);
CHECK(StrCompareIgnoreCase(base.substr(3, 1), base.substr(2, 1)) > 0);
CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(3, 1)) < 0);
CHECK(StrCompareIgnoreCase(base.substr(4, 1), base.substr(0, 1)) > 0);
CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(0, 2)) < 0); // Same position, different lengths
CHECK(StrCompareIgnoreCase(base.substr(0, 2), base.substr(0, 1)) > 0); // Same position, different lengths
}
TEST_CASE("StrEqualsIgnoreCase - std::string")
{
/* Same string, with different cases. */
CHECK(StrEqualsIgnoreCase(std::string{""}, std::string{""}));
CHECK(StrEqualsIgnoreCase(std::string{"a"}, std::string{"a"}));
CHECK(StrEqualsIgnoreCase(std::string{"a"}, std::string{"A"}));
CHECK(StrEqualsIgnoreCase(std::string{"A"}, std::string{"a"}));
CHECK(StrEqualsIgnoreCase(std::string{"A"}, std::string{"A"}));
/* Not the same string. */
CHECK(!StrEqualsIgnoreCase(std::string{""}, std::string{"b"}));
CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{""}));
CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{"b"}));
CHECK(!StrEqualsIgnoreCase(std::string{"b"}, std::string{"a"}));
CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{"aa"}));
CHECK(!StrEqualsIgnoreCase(std::string{"aa"}, std::string{"a"}));
}
TEST_CASE("StrEqualsIgnoreCase - char pointer")
{
/* Same string, with different cases. */
CHECK(StrEqualsIgnoreCase("", ""));
CHECK(StrEqualsIgnoreCase("a", "a"));
CHECK(StrEqualsIgnoreCase("a", "A"));
CHECK(StrEqualsIgnoreCase("A", "a"));
CHECK(StrEqualsIgnoreCase("A", "A"));
/* Not the same string. */
CHECK(!StrEqualsIgnoreCase("", "b"));
CHECK(!StrEqualsIgnoreCase("a", ""));
CHECK(!StrEqualsIgnoreCase("a", "b"));
CHECK(!StrEqualsIgnoreCase("b", "a"));
CHECK(!StrEqualsIgnoreCase("a", "aa"));
CHECK(!StrEqualsIgnoreCase("aa", "a"));
}
TEST_CASE("StrEqualsIgnoreCase - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aaAb"};
/* Same string, with different cases. */
CHECK(StrEqualsIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions
CHECK(StrEqualsIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions
CHECK(StrEqualsIgnoreCase(base.substr(0, 1), base.substr(2, 1)));
CHECK(StrEqualsIgnoreCase(base.substr(2, 1), base.substr(1, 1)));
CHECK(StrEqualsIgnoreCase(base.substr(2, 1), base.substr(2, 1)));
/* Not the same string. */
CHECK(!StrEqualsIgnoreCase(base.substr(3, 0), base.substr(3, 1))); // Same position, different lengths
CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(0, 0)));
CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(3, 1)));
CHECK(!StrEqualsIgnoreCase(base.substr(3, 1), base.substr(0, 1)));
CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(0, 2))); // Same position, different lengths
CHECK(!StrEqualsIgnoreCase(base.substr(0, 2), base.substr(0, 1))); // Same position, different lengths
}
/**** String starts with *****/
TEST_CASE("StrStartsWith - std::string")
{
/* Everything starts with an empty prefix. */
CHECK(StrStartsWith(std::string{""}, std::string{""}));
CHECK(StrStartsWith(std::string{"a"}, std::string{""}));
/* Equal strings. */
CHECK(StrStartsWith(std::string{"a"}, std::string{"a"}));
CHECK(StrStartsWith(std::string{"A"}, std::string{"A"}));
/* Starts with same. */
CHECK(StrStartsWith(std::string{"ab"}, std::string{"a"}));
CHECK(StrStartsWith(std::string{"Ab"}, std::string{"A"}));
/* Different cases. */
CHECK(!StrStartsWith(std::string{"a"}, std::string{"A"}));
CHECK(!StrStartsWith(std::string{"A"}, std::string{"a"}));
CHECK(!StrStartsWith(std::string{"ab"}, std::string{"A"}));
CHECK(!StrStartsWith(std::string{"Ab"}, std::string{"a"}));
/* Does not start the same. */
CHECK(!StrStartsWith(std::string{""}, std::string{"b"}));
CHECK(!StrStartsWith(std::string{"a"}, std::string{"b"}));
CHECK(!StrStartsWith(std::string{"b"}, std::string{"a"}));
CHECK(!StrStartsWith(std::string{"a"}, std::string{"aa"}));
}
TEST_CASE("StrStartsWith - char pointer")
{
CHECK(StrStartsWith("", ""));
CHECK(StrStartsWith("a", ""));
/* Equal strings. */
CHECK(StrStartsWith("a", "a"));
CHECK(StrStartsWith("A", "A"));
/* Starts with same. */
CHECK(StrStartsWith("ab", "a"));
CHECK(StrStartsWith("Ab", "A"));
/* Different cases. */
CHECK(!StrStartsWith("a", "A"));
CHECK(!StrStartsWith("A", "a"));
CHECK(!StrStartsWith("ab", "A"));
CHECK(!StrStartsWith("Ab", "a"));
/* Does not start the same. */
CHECK(!StrStartsWith("", "b"));
CHECK(!StrStartsWith("a", "b"));
CHECK(!StrStartsWith("b", "a"));
CHECK(!StrStartsWith("a", "aa"));
}
TEST_CASE("StrStartsWith - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aabAb"};
/* Everything starts with an empty prefix. */
CHECK(StrStartsWith(base.substr(0, 0), base.substr(1, 0))); // Different positions
CHECK(StrStartsWith(base.substr(0, 1), base.substr(0, 0)));
/* Equals string. */
CHECK(StrStartsWith(base.substr(0, 1), base.substr(1, 1))); // Different positions
CHECK(StrStartsWith(base.substr(3, 1), base.substr(3, 1)));
/* Starts with same. */
CHECK(StrStartsWith(base.substr(1, 2), base.substr(0, 1)));
CHECK(StrStartsWith(base.substr(3, 2), base.substr(3, 1)));
/* Different cases. */
CHECK(!StrStartsWith(base.substr(0, 1), base.substr(3, 1)));
CHECK(!StrStartsWith(base.substr(3, 1), base.substr(0, 1)));
CHECK(!StrStartsWith(base.substr(1, 2), base.substr(3, 1)));
CHECK(!StrStartsWith(base.substr(3, 2), base.substr(0, 1)));
/* Does not start the same. */
CHECK(!StrStartsWith(base.substr(2, 0), base.substr(2, 1)));
CHECK(!StrStartsWith(base.substr(0, 1), base.substr(2, 1)));
CHECK(!StrStartsWith(base.substr(2, 1), base.substr(0, 1)));
CHECK(!StrStartsWith(base.substr(0, 1), base.substr(0, 2)));
}
TEST_CASE("StrStartsWithIgnoreCase - std::string")
{
/* Everything starts with an empty prefix. */
CHECK(StrStartsWithIgnoreCase(std::string{""}, std::string{""}));
CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{""}));
/* Equals string, ignoring case. */
CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{"a"}));
CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{"A"}));
CHECK(StrStartsWithIgnoreCase(std::string{"A"}, std::string{"a"}));
CHECK(StrStartsWithIgnoreCase(std::string{"A"}, std::string{"A"}));
/* Starts with same, ignoring case. */
CHECK(StrStartsWithIgnoreCase(std::string{"ab"}, std::string{"a"}));
CHECK(StrStartsWithIgnoreCase(std::string{"ab"}, std::string{"A"}));
CHECK(StrStartsWithIgnoreCase(std::string{"Ab"}, std::string{"a"}));
CHECK(StrStartsWithIgnoreCase(std::string{"Ab"}, std::string{"A"}));
/* Does not start the same. */
CHECK(!StrStartsWithIgnoreCase(std::string{""}, std::string{"b"}));
CHECK(!StrStartsWithIgnoreCase(std::string{"a"}, std::string{"b"}));
CHECK(!StrStartsWithIgnoreCase(std::string{"b"}, std::string{"a"}));
CHECK(!StrStartsWithIgnoreCase(std::string{"a"}, std::string{"aa"}));
}
TEST_CASE("StrStartsWithIgnoreCase - char pointer")
{
/* Everything starts with an empty prefix. */
CHECK(StrStartsWithIgnoreCase("", ""));
CHECK(StrStartsWithIgnoreCase("a", ""));
/* Equals string, ignoring case. */
CHECK(StrStartsWithIgnoreCase("a", "a"));
CHECK(StrStartsWithIgnoreCase("a", "A"));
CHECK(StrStartsWithIgnoreCase("A", "a"));
CHECK(StrStartsWithIgnoreCase("A", "A"));
/* Starts with same, ignoring case. */
CHECK(StrStartsWithIgnoreCase("ab", "a"));
CHECK(StrStartsWithIgnoreCase("ab", "A"));
CHECK(StrStartsWithIgnoreCase("Ab", "a"));
CHECK(StrStartsWithIgnoreCase("Ab", "A"));
/* Does not start the same. */
CHECK(!StrStartsWithIgnoreCase("", "b"));
CHECK(!StrStartsWithIgnoreCase("a", "b"));
CHECK(!StrStartsWithIgnoreCase("b", "a"));
CHECK(!StrStartsWithIgnoreCase("a", "aa"));
}
TEST_CASE("StrStartsWithIgnoreCase - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aabAb"};
/* Everything starts with an empty prefix. */
CHECK(StrStartsWithIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions
CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(0, 0)));
/* Equals string, ignoring case. */
CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions
CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(3, 1)));
CHECK(StrStartsWithIgnoreCase(base.substr(3, 1), base.substr(0, 1)));
CHECK(StrStartsWithIgnoreCase(base.substr(3, 1), base.substr(3, 1)));
/* Starts with same, ignoring case. */
CHECK(StrStartsWithIgnoreCase(base.substr(1, 2), base.substr(0, 1)));
CHECK(StrStartsWithIgnoreCase(base.substr(1, 2), base.substr(3, 1)));
CHECK(StrStartsWithIgnoreCase(base.substr(3, 2), base.substr(0, 1)));
CHECK(StrStartsWithIgnoreCase(base.substr(3, 2), base.substr(3, 1)));
/* Does not start the same. */
CHECK(!StrStartsWithIgnoreCase(base.substr(2, 0), base.substr(2, 1)));
CHECK(!StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(2, 1)));
CHECK(!StrStartsWithIgnoreCase(base.substr(2, 1), base.substr(0, 1)));
CHECK(!StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(0, 2)));
}
/**** String ends with *****/
TEST_CASE("StrEndsWith - std::string")
{
/* Everything ends with an empty prefix. */
CHECK(StrEndsWith(std::string{""}, std::string{""}));
CHECK(StrEndsWith(std::string{"a"}, std::string{""}));
/* Equal strings. */
CHECK(StrEndsWith(std::string{"a"}, std::string{"a"}));
CHECK(StrEndsWith(std::string{"A"}, std::string{"A"}));
/* Ends with same. */
CHECK(StrEndsWith(std::string{"ba"}, std::string{"a"}));
CHECK(StrEndsWith(std::string{"bA"}, std::string{"A"}));
/* Different cases. */
CHECK(!StrEndsWith(std::string{"a"}, std::string{"A"}));
CHECK(!StrEndsWith(std::string{"A"}, std::string{"a"}));
CHECK(!StrEndsWith(std::string{"ba"}, std::string{"A"}));
CHECK(!StrEndsWith(std::string{"bA"}, std::string{"a"}));
/* Does not end the same. */
CHECK(!StrEndsWith(std::string{""}, std::string{"b"}));
CHECK(!StrEndsWith(std::string{"a"}, std::string{"b"}));
CHECK(!StrEndsWith(std::string{"b"}, std::string{"a"}));
CHECK(!StrEndsWith(std::string{"a"}, std::string{"aa"}));
}
TEST_CASE("StrEndsWith - char pointer")
{
CHECK(StrEndsWith("", ""));
CHECK(StrEndsWith("a", ""));
/* Equal strings. */
CHECK(StrEndsWith("a", "a"));
CHECK(StrEndsWith("A", "A"));
/* Ends with same. */
CHECK(StrEndsWith("ba", "a"));
CHECK(StrEndsWith("bA", "A"));
/* Different cases. */
CHECK(!StrEndsWith("a", "A"));
CHECK(!StrEndsWith("A", "a"));
CHECK(!StrEndsWith("ba", "A"));
CHECK(!StrEndsWith("bA", "a"));
/* Does not end the same. */
CHECK(!StrEndsWith("", "b"));
CHECK(!StrEndsWith("a", "b"));
CHECK(!StrEndsWith("b", "a"));
CHECK(!StrEndsWith("a", "aa"));
}
TEST_CASE("StrEndsWith - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aabAba"};
/* Everything ends with an empty prefix. */
CHECK(StrEndsWith(base.substr(0, 0), base.substr(1, 0))); // Different positions
CHECK(StrEndsWith(base.substr(0, 1), base.substr(0, 0)));
/* Equals string. */
CHECK(StrEndsWith(base.substr(0, 1), base.substr(1, 1))); // Different positions
CHECK(StrEndsWith(base.substr(3, 1), base.substr(3, 1)));
/* Ends with same. */
CHECK(StrEndsWith(base.substr(4, 2), base.substr(0, 1)));
CHECK(StrEndsWith(base.substr(2, 2), base.substr(3, 1)));
/* Different cases. */
CHECK(!StrEndsWith(base.substr(0, 1), base.substr(3, 1)));
CHECK(!StrEndsWith(base.substr(3, 1), base.substr(0, 1)));
CHECK(!StrEndsWith(base.substr(4, 2), base.substr(3, 1)));
CHECK(!StrEndsWith(base.substr(2, 2), base.substr(0, 1)));
/* Does not end the same. */
CHECK(!StrEndsWith(base.substr(2, 0), base.substr(2, 1)));
CHECK(!StrEndsWith(base.substr(0, 1), base.substr(2, 1)));
CHECK(!StrEndsWith(base.substr(2, 1), base.substr(0, 1)));
CHECK(!StrEndsWith(base.substr(0, 1), base.substr(0, 2)));
}
TEST_CASE("StrEndsWithIgnoreCase - std::string")
{
/* Everything ends with an empty prefix. */
CHECK(StrEndsWithIgnoreCase(std::string{""}, std::string{""}));
CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{""}));
/* Equals string, ignoring case. */
CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{"a"}));
CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{"A"}));
CHECK(StrEndsWithIgnoreCase(std::string{"A"}, std::string{"a"}));
CHECK(StrEndsWithIgnoreCase(std::string{"A"}, std::string{"A"}));
/* Ends with same, ignoring case. */
CHECK(StrEndsWithIgnoreCase(std::string{"ba"}, std::string{"a"}));
CHECK(StrEndsWithIgnoreCase(std::string{"ba"}, std::string{"A"}));
CHECK(StrEndsWithIgnoreCase(std::string{"bA"}, std::string{"a"}));
CHECK(StrEndsWithIgnoreCase(std::string{"bA"}, std::string{"A"}));
/* Does not end the same. */
CHECK(!StrEndsWithIgnoreCase(std::string{""}, std::string{"b"}));
CHECK(!StrEndsWithIgnoreCase(std::string{"a"}, std::string{"b"}));
CHECK(!StrEndsWithIgnoreCase(std::string{"b"}, std::string{"a"}));
CHECK(!StrEndsWithIgnoreCase(std::string{"a"}, std::string{"aa"}));
}
TEST_CASE("StrEndsWithIgnoreCase - char pointer")
{
/* Everything ends with an empty prefix. */
CHECK(StrEndsWithIgnoreCase("", ""));
CHECK(StrEndsWithIgnoreCase("a", ""));
/* Equals string, ignoring case. */
CHECK(StrEndsWithIgnoreCase("a", "a"));
CHECK(StrEndsWithIgnoreCase("a", "A"));
CHECK(StrEndsWithIgnoreCase("A", "a"));
CHECK(StrEndsWithIgnoreCase("A", "A"));
/* Ends with same, ignoring case. */
CHECK(StrEndsWithIgnoreCase("ba", "a"));
CHECK(StrEndsWithIgnoreCase("ba", "A"));
CHECK(StrEndsWithIgnoreCase("bA", "a"));
CHECK(StrEndsWithIgnoreCase("bA", "A"));
/* Does not end the same. */
CHECK(!StrEndsWithIgnoreCase("", "b"));
CHECK(!StrEndsWithIgnoreCase("a", "b"));
CHECK(!StrEndsWithIgnoreCase("b", "a"));
CHECK(!StrEndsWithIgnoreCase("a", "aa"));
}
TEST_CASE("StrEndsWithIgnoreCase - std::string_view")
{
/*
* With std::string_view the only way to access the data is via .data(),
* which does not guarantee the termination that would be required by
* things such as stricmp/strcasecmp. So, just passing .data() into stricmp
* or strcasecmp would fail if it does not account for the length of the
* view. Thus, contrary to the string/char* tests, this uses the same base
* string but gets different sections to trigger these corner cases.
*/
std::string_view base{"aabAba"};
/* Everything ends with an empty prefix. */
CHECK(StrEndsWithIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions
CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(0, 0)));
/* Equals string, ignoring case. */
CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions
CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(3, 1)));
CHECK(StrEndsWithIgnoreCase(base.substr(3, 1), base.substr(0, 1)));
CHECK(StrEndsWithIgnoreCase(base.substr(3, 1), base.substr(3, 1)));
/* Ends with same, ignoring case. */
CHECK(StrEndsWithIgnoreCase(base.substr(2, 2), base.substr(0, 1)));
CHECK(StrEndsWithIgnoreCase(base.substr(2, 2), base.substr(3, 1)));
CHECK(StrEndsWithIgnoreCase(base.substr(4, 2), base.substr(0, 1)));
CHECK(StrEndsWithIgnoreCase(base.substr(4, 2), base.substr(3, 1)));
/* Does not end the same. */
CHECK(!StrEndsWithIgnoreCase(base.substr(2, 0), base.substr(2, 1)));
CHECK(!StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(2, 1)));
CHECK(!StrEndsWithIgnoreCase(base.substr(2, 1), base.substr(0, 1)));
CHECK(!StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(0, 2)));
}
TEST_CASE("FormatArrayAsHex")
{
CHECK(FormatArrayAsHex(std::array<byte, 0>{}) == "");
CHECK(FormatArrayAsHex(std::array<byte, 1>{0x12}) == "12");
CHECK(FormatArrayAsHex(std::array<byte, 4>{0x13, 0x38, 0x42, 0xAF}) == "133842af");
}

@ -0,0 +1,52 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file strings_func.cpp Test functionality from strings_func. */
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
#include "../strings_func.h"
TEST_CASE("HaveDParamChanged")
{
SetDParam(0, 0);
SetDParamStr(1, "some string");
std::vector<StringParameterBackup> backup;
CopyOutDParam(backup, 2);
CHECK(HaveDParamChanged(backup) == false);
/* A different parameter 0 (both string and numeric). */
SetDParam(0, 1);
CHECK(HaveDParamChanged(backup) == true);
SetDParamStr(0, "some other string");
CHECK(HaveDParamChanged(backup) == true);
/* Back to the original state, nothing should have changed. */
SetDParam(0, 0);
CHECK(HaveDParamChanged(backup) == false);
/* A different parameter 1 (both string and numeric). */
SetDParamStr(1, "some other string");
CHECK(HaveDParamChanged(backup) == true);
SetDParam(1, 0);
CHECK(HaveDParamChanged(backup) == true);
/* Back to the original state, nothing should have changed. */
SetDParamStr(1, "some string");
CHECK(HaveDParamChanged(backup) == false);
/* Changing paramter 2 should not have any effect, as the backup is only 2 long. */
SetDParam(2, 3);
CHECK(HaveDParamChanged(backup) == false);
}

@ -7,24 +7,8 @@
/** @file test_main.cpp Entry point for all the unit tests. */
#define OPENTTD_TEST
#include "../stdafx.h"
#include <stdarg.h>
#include <stdio.h>
#define CATCH_CONFIG_MAIN
#define DO_NOT_USE_WMAIN
#include "../3rdparty/catch2/catch.hpp"
void CDECL error(const char *s, ...)
{
va_list va;
char buffer[1024];
va_start(va, s);
vsnprintf(buffer, 1024, s, va);
va_end(va);
CATCH_RUNTIME_ERROR(buffer);
}

@ -0,0 +1,181 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file script_admin_json.cpp Tests for the Squirrel -> JSON conversion in ScriptAdmin. */
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
#include "../game/game_instance.hpp"
#include "../script/api/script_admin.hpp"
#include "../script/api/script_event_types.hpp"
#include "../script/script_instance.hpp"
#include "../script/squirrel.hpp"
#include "../core/format.hpp"
#include "../3rdparty/nlohmann/json.hpp"
#include <squirrel.h>
/**
* A controller to start enough so we can use Squirrel for testing.
*
* To run Squirrel, we need an Allocator, so malloc/free works.
* For functions that log, we need an ActiveInstance, so the logger knows where
* to send the logs to.
*
* By instantiating this class, both are set correctly. After that you can
* use Squirrel without issues.
*/
class TestScriptController {
public:
GameInstance game{};
ScriptObject::ActiveInstance active{&game};
Squirrel engine{"test"};
ScriptAllocatorScope scope{&engine};
};
extern bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0);
/**
* Small wrapper around ScriptAdmin's MakeJSON that prepares the Squirrel
* engine if it was called from actual scripting..
*/
static std::optional<std::string> TestScriptAdminMakeJSON(std::string_view squirrel)
{
auto vm = sq_open(1024);
/* sq_compile creates a closure with our snipper, which is a table.
* Add "return " to get the table on the stack. */
std::string buffer = fmt::format("return {}", squirrel);
/* Insert an (empty) class for testing. */
sq_pushroottable(vm);
sq_pushstring(vm, "DummyClass", -1);
sq_newclass(vm, SQFalse);
sq_newslot(vm, -3, SQFalse);
sq_pop(vm, 1);
/* Compile the snippet. */
REQUIRE(sq_compilebuffer(vm, buffer.c_str(), buffer.size(), "test", SQTrue) == SQ_OK);
/* Execute the snippet, capturing the return value. */
sq_pushroottable(vm);
REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK);
/* Ensure the snippet pushed a table on the stack. */
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
/* Feed the snippet into the MakeJSON function. */
nlohmann::json json;
if (!ScriptAdminMakeJSON(json, vm, -1)) {
sq_close(vm);
return std::nullopt;
}
sq_close(vm);
return json.dump();
}
/**
* Validate ScriptEventAdminPort can convert JSON to Squirrel.
*
* This function is not actually part of ScriptAdmin, but we will use MakeJSON,
* and as such need to be inside this class.
*
* The easiest way to do validate, is to first use ScriptEventAdminPort (the function
* we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON
* to convert it back to JSON.
*
* Sadly, Squirrel has no way to easily compare if two tables are identical, so we
* use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you,
* a failure in the final JSON might also mean a bug in MakeJSON.
*
* @param json The JSON-string to convert to Squirrel
* @return The Squirrel table converted to a JSON-string.
*/
static std::optional<std::string> TestScriptEventAdminPort(const std::string &json)
{
auto vm = sq_open(1024);
/* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */
ScriptEventAdminPort(json).GetObject(vm);
if (sq_gettype(vm, -1) == OT_NULL) {
sq_close(vm);
return std::nullopt;
}
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
nlohmann::json squirrel_json;
REQUIRE(ScriptAdminMakeJSON(squirrel_json, vm, -1) == true);
sq_close(vm);
return squirrel_json.dump();
}
TEST_CASE("Squirrel -> JSON conversion")
{
TestScriptController controller;
CHECK(TestScriptAdminMakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json");
/* Cases that should fail, as we cannot convert a class to JSON. */
CHECK(TestScriptAdminMakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt);
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt);
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt);
}
TEST_CASE("JSON -> Squirrel conversion")
{
TestScriptController controller;
CHECK(TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json");
/* Check if spaces are properly ignored. */
CHECK(TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json");
CHECK(TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json");
/* Valid JSON but invalid Squirrel (read: floats). */
CHECK(TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt);
/* Root element has to be an object. */
CHECK(TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( null )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( true )json") == std::nullopt);
/* Cases that should fail, as it is invalid JSON. */
CHECK(TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing }
CHECK(TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening {
CHECK(TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ]
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [
}

@ -0,0 +1,101 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file test_window_desc.cpp Test WindowDescs for valid widget parts. */
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
#include "mock_environment.h"
#include "../window_gui.h"
#include "../core/format.hpp"
#include <set>
/**
* List of WindowDescs. Defined in window.cpp but not exposed as this unit-test is the only other place that needs it.
* WindowDesc is a self-registering class so all WindowDescs will be included in the list.
*/
extern std::vector<WindowDesc*> *_window_descs;
class WindowDescTestsFixture {
private:
MockEnvironment &mock = MockEnvironment::Instance();
};
TEST_CASE("WindowDesc - ini_key uniqueness")
{
std::set<std::string> seen;
for (const WindowDesc *window_desc : *_window_descs) {
if (window_desc->ini_key == nullptr) continue;
CAPTURE(window_desc->ini_key);
CHECK((seen.find(window_desc->ini_key) == std::end(seen)));
seen.insert(window_desc->ini_key);
}
}
TEST_CASE("WindowDesc - ini_key validity")
{
const WindowDesc *window_desc = GENERATE(from_range(std::begin(*_window_descs), std::end(*_window_descs)));
bool has_inikey = window_desc->ini_key != nullptr;
bool has_widget = std::any_of(window_desc->nwid_begin, window_desc->nwid_end, [](const NWidgetPart &part) { return part.type == WWT_DEFSIZEBOX || part.type == WWT_STICKYBOX; });
INFO(fmt::format("{}:{}", window_desc->file, window_desc->line));
CAPTURE(has_inikey);
CAPTURE(has_widget);
CHECK((has_widget == has_inikey));
}
/**
* Test if a NWidgetTree is properly closed, meaning the number of container-type parts matches the number of
* EndContainer() parts.
* @param nwid_begin Pointer to beginning of nested widget parts.
* @param nwid_end Pointer to ending of nested widget parts.
* @return True iff nested tree is properly closed.
*/
static bool IsNWidgetTreeClosed(const NWidgetPart *nwid_begin, const NWidgetPart *nwid_end)
{
int depth = 0;
for (; nwid_begin < nwid_end; ++nwid_begin) {
if (IsContainerWidgetType(nwid_begin->type)) ++depth;
if (nwid_begin->type == WPT_ENDCONTAINER) --depth;
}
return depth == 0;
}
TEST_CASE("WindowDesc - NWidgetParts properly closed")
{
const WindowDesc *window_desc = GENERATE(from_range(std::begin(*_window_descs), std::end(*_window_descs)));
INFO(fmt::format("{}:{}", window_desc->file, window_desc->line));
CHECK(IsNWidgetTreeClosed(window_desc->nwid_begin, window_desc->nwid_end));
}
TEST_CASE_METHOD(WindowDescTestsFixture, "WindowDesc - NWidgetPart validity")
{
const WindowDesc *window_desc = GENERATE(from_range(std::begin(*_window_descs), std::end(*_window_descs)));
INFO(fmt::format("{}:{}", window_desc->file, window_desc->line));
int biggest_index = -1;
NWidgetStacked *shade_select = nullptr;
NWidgetBase *root = nullptr;
REQUIRE_NOTHROW(root = MakeWindowNWidgetTree(window_desc->nwid_begin, window_desc->nwid_end, &biggest_index, &shade_select));
CHECK((root != nullptr));
}

@ -94,7 +94,7 @@ std::bitset<WC_END> _present_window_types;
* List of all WindowDescs.
* This is a pointer to ensure initialisation order with the various static WindowDesc instances.
*/
static std::vector<WindowDesc*> *_window_descs = nullptr;
std::vector<WindowDesc*> *_window_descs = nullptr;
/** Config file to store WindowDesc */
std::string _windows_file;

Loading…
Cancel
Save