diff --git a/src/fontcache.h b/src/fontcache.h
index 2204c1aea0..0d6596289c 100644
--- a/src/fontcache.h
+++ b/src/fontcache.h
@@ -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:
diff --git a/src/os/macosx/CMakeLists.txt b/src/os/macosx/CMakeLists.txt
index eb5a727990..ef5d90b15d 100644
--- a/src/os/macosx/CMakeLists.txt
+++ b/src/os/macosx/CMakeLists.txt
@@ -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()
diff --git a/src/os/macosx/osx_main.cpp b/src/os/macosx/osx_main.cpp
new file mode 100644
index 0000000000..cfbf8aa451
--- /dev/null
+++ b/src/os/macosx/osx_main.cpp
@@ -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 .
+ */
+
+/** @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
+#include
+
+#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;
+}
diff --git a/src/os/unix/CMakeLists.txt b/src/os/unix/CMakeLists.txt
index c38b65deaf..1e8bb5d63d 100644
--- a/src/os/unix/CMakeLists.txt
+++ b/src/os/unix/CMakeLists.txt
@@ -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()
diff --git a/src/os/unix/unix.cpp b/src/os/unix/unix.cpp
index 2a797192b6..5142ab62ea 100644
--- a/src/os/unix/unix.cpp
+++ b/src/os/unix/unix.cpp
@@ -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
-# 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 GetClipboardContents()
{
diff --git a/src/os/unix/unix_main.cpp b/src/os/unix/unix_main.cpp
new file mode 100644
index 0000000000..8ac38f2cbf
--- /dev/null
+++ b/src/os/unix/unix_main.cpp
@@ -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 .
+ */
+
+/** @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
+#include
+
+#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);
+}
diff --git a/src/os/windows/CMakeLists.txt b/src/os/windows/CMakeLists.txt
index 145d3b5242..9215514fa2 100644
--- a/src/os/windows/CMakeLists.txt
+++ b/src/os/windows/CMakeLists.txt
@@ -9,3 +9,7 @@ add_files(
win32.h
CONDITION WIN32
)
+
+if(WIN32)
+ target_sources(openttd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/win32_main.cpp)
+endif()
diff --git a/src/os/windows/win32.cpp b/src/os/windows/win32.cpp
index 709b8fec88..88652a52ee 100644
--- a/src/os/windows/win32.cpp
+++ b/src/os/windows/win32.cpp
@@ -270,37 +270,6 @@ std::optional 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];
diff --git a/src/os/windows/win32_main.cpp b/src/os/windows/win32_main.cpp
new file mode 100644
index 0000000000..77ac95ee3c
--- /dev/null
+++ b/src/os/windows/win32_main.cpp
@@ -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 .
+ */
+
+/** @file win32_main.cpp Implementation main for Windows. */
+
+#include "../../stdafx.h"
+#include
+#include
+#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;
+}
diff --git a/src/stdafx.h b/src/stdafx.h
index 6bb69359df..e03689557e 100644
--- a/src/stdafx.h
+++ b/src/stdafx.h
@@ -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
@@ -220,7 +220,7 @@
template std::string FS2OTTD(T name) { return name; }
template 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 "\\"
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index dd0ec5dbb8..aece643047 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -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
)
diff --git a/src/tests/bitmath_func.cpp b/src/tests/bitmath_func.cpp
index dfd6365f78..5f5f7c79bb 100644
--- a/src/tests/bitmath_func.cpp
+++ b/src/tests/bitmath_func.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"
diff --git a/src/tests/landscape_partial_pixel_z.cpp b/src/tests/landscape_partial_pixel_z.cpp
index 26b7c70892..f5c22786f7 100644
--- a/src/tests/landscape_partial_pixel_z.cpp
+++ b/src/tests/landscape_partial_pixel_z.cpp
@@ -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"
diff --git a/src/tests/math_func.cpp b/src/tests/math_func.cpp
index 2b2ecaf24e..769e344145 100644
--- a/src/tests/math_func.cpp
+++ b/src/tests/math_func.cpp
@@ -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 .
*/
-/** @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(127));
CHECK(126 == ClampTo(126));
- CHECK(126 == ClampTo(static_cast(126)));
- CHECK(126 == ClampTo(static_cast(126)));
- CHECK(0 == ClampTo(static_cast(-126)));
- CHECK(0 == ClampTo(static_cast(-126)));
+ CHECK(126 == ClampTo(static_cast(126)));
+ CHECK(126 == ClampTo(static_cast(126)));
+ CHECK(0 == ClampTo(static_cast(-126)));
+ CHECK(0 == ClampTo(static_cast(-126)));
/* The realm around 64 bits types is tricky as there is not one type/method that works for all. */
diff --git a/src/tests/mock_environment.h b/src/tests/mock_environment.h
new file mode 100644
index 0000000000..470914d002
--- /dev/null
+++ b/src/tests/mock_environment.h
@@ -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 .
+ */
+
+/** @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 */
diff --git a/src/tests/mock_fontcache.h b/src/tests/mock_fontcache.h
new file mode 100644
index 0000000000..c9eb326029
--- /dev/null
+++ b/src/tests/mock_fontcache.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 .
+ */
+
+/** @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 */
diff --git a/src/tests/mock_spritecache.cpp b/src/tests/mock_spritecache.cpp
new file mode 100644
index 0000000000..09d5161dbb
--- /dev/null
+++ b/src/tests/mock_spritecache.cpp
@@ -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 .
+ */
+
+/** @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++;
+ }
+}
diff --git a/src/tests/mock_spritecache.h b/src/tests/mock_spritecache.h
new file mode 100644
index 0000000000..6d715cf699
--- /dev/null
+++ b/src/tests/mock_spritecache.h
@@ -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 .
+ */
+
+/** @file mock_spritecache.h Mock sprite cache definition. */
+
+#ifndef MOCK_SPRITECACHE_H
+#define MOCK_SPRITECACHE_H
+
+void MockGfxLoadSprites();
+
+#endif /* MOCK_SPRITECACHE_H */
diff --git a/src/tests/ring_buffer.cpp b/src/tests/ring_buffer.cpp
index 1de6ecea37..5075af538b 100644
--- a/src/tests/ring_buffer.cpp
+++ b/src/tests/ring_buffer.cpp
@@ -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"
diff --git a/src/tests/string_func.cpp b/src/tests/string_func.cpp
new file mode 100644
index 0000000000..19a8fb26d4
--- /dev/null
+++ b/src/tests/string_func.cpp
@@ -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 .
+ */
+
+/** @file string_func.cpp Test functionality from string_func. */
+
+#include "../stdafx.h"
+
+#include "../3rdparty/catch2/catch.hpp"
+
+#include "../string_func.h"
+#include
+
+/**** 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{}) == "");
+ CHECK(FormatArrayAsHex(std::array{0x12}) == "12");
+ CHECK(FormatArrayAsHex(std::array{0x13, 0x38, 0x42, 0xAF}) == "133842af");
+}
diff --git a/src/tests/strings_func.cpp b/src/tests/strings_func.cpp
new file mode 100644
index 0000000000..dd70b0a365
--- /dev/null
+++ b/src/tests/strings_func.cpp
@@ -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 .
+ */
+
+/** @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 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);
+
+}
diff --git a/src/tests/test_main.cpp b/src/tests/test_main.cpp
index 8a4d058fbc..fbade1e6ab 100644
--- a/src/tests/test_main.cpp
+++ b/src/tests/test_main.cpp
@@ -7,24 +7,8 @@
/** @file test_main.cpp Entry point for all the unit tests. */
-#define OPENTTD_TEST
#include "../stdafx.h"
-#include
-#include
-
#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);
-}
diff --git a/src/tests/test_script_admin.cpp b/src/tests/test_script_admin.cpp
new file mode 100644
index 0000000000..7826b3d23c
--- /dev/null
+++ b/src/tests/test_script_admin.cpp
@@ -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 .
+ */
+
+/** @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
+
+/**
+ * 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 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 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 [
+}
diff --git a/src/tests/test_window_desc.cpp b/src/tests/test_window_desc.cpp
new file mode 100644
index 0000000000..71277581a0
--- /dev/null
+++ b/src/tests/test_window_desc.cpp
@@ -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 .
+ */
+
+/** @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
+
+/**
+ * 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 *_window_descs;
+
+
+class WindowDescTestsFixture {
+private:
+ MockEnvironment &mock = MockEnvironment::Instance();
+};
+
+
+TEST_CASE("WindowDesc - ini_key uniqueness")
+{
+ std::set 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));
+}
diff --git a/src/window.cpp b/src/window.cpp
index a91ff3c499..e5fc217638 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -94,7 +94,7 @@ std::bitset _present_window_types;
* List of all WindowDescs.
* This is a pointer to ensure initialisation order with the various static WindowDesc instances.
*/
-static std::vector *_window_descs = nullptr;
+std::vector *_window_descs = nullptr;
/** Config file to store WindowDesc */
std::string _windows_file;