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;