From 0813602a41b1435b05f33e393fba70dd805ddd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Schu=CC=88mann?= Date: Tue, 28 Apr 2026 16:15:40 +0200 Subject: [PATCH 1/2] Add unit tests for CBlockBase, CReport parsing, CReportStream, and MetaScript functionality. Update build configuration to support test fixtures and temporary directories. --- EBase/test/CMakeLists.txt | 19 ++- EBase/test/blockbase_tests.cpp | 40 +++++ EBase/test/expression_tests.cpp | 2 +- EBase/test/fixtures/test-v69.cr | 249 ++++++++++++++++++++++++++++++ EBase/test/report_parse_tests.cpp | 82 ++++++++++ EBase/test/reportstream_tests.cpp | 102 ++++++++++++ EBase/test/test_utils.h | 95 ++++++++++++ Vorlage/CMakeLists.txt | 23 ++- Vorlage/test/CMakeLists.txt | 22 +++ Vorlage/test/metascript_tests.cpp | 140 +++++++++++++++++ 10 files changed, 768 insertions(+), 6 deletions(-) create mode 100644 EBase/test/blockbase_tests.cpp create mode 100644 EBase/test/fixtures/test-v69.cr create mode 100644 EBase/test/report_parse_tests.cpp create mode 100644 EBase/test/reportstream_tests.cpp create mode 100644 EBase/test/test_utils.h create mode 100644 Vorlage/test/CMakeLists.txt create mode 100644 Vorlage/test/metascript_tests.cpp diff --git a/EBase/test/CMakeLists.txt b/EBase/test/CMakeLists.txt index 3dec28f..35fa055 100644 --- a/EBase/test/CMakeLists.txt +++ b/EBase/test/CMakeLists.txt @@ -1,6 +1,18 @@ -add_executable(ebase_catch_tests ctest_main.cpp expression_tests.cpp value_tests.cpp charencoding_test.cpp) +add_executable(ebase_catch_tests + ctest_main.cpp + expression_tests.cpp + value_tests.cpp + charencoding_test.cpp + reportstream_tests.cpp + blockbase_tests.cpp + report_parse_tests.cpp +) target_link_libraries(ebase_catch_tests ebase Catch2::Catch2WithMain) +target_compile_definitions(ebase_catch_tests PRIVATE + EBASE_TEST_CONFIG_FILE="${CMAKE_SOURCE_DIR}/config/eressea.cfg" + EBASE_TEST_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures" +) if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) target_compile_definitions(ebase_catch_tests PRIVATE _CRT_SECURE_NO_WARNINGS) @@ -8,5 +20,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) target_compile_options(ebase_catch_tests PRIVATE "$<$:/utf-8>") endif() +if(APPLE) + target_compile_options(ebase_catch_tests PRIVATE -mmacosx-version-min=10.15) + target_link_options(ebase_catch_tests PRIVATE -mmacosx-version-min=10.15) +endif() + include(Catch) catch_discover_tests(ebase_catch_tests) diff --git a/EBase/test/blockbase_tests.cpp b/EBase/test/blockbase_tests.cpp new file mode 100644 index 0000000..ce2503f --- /dev/null +++ b/EBase/test/blockbase_tests.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include +#include + +TEST_CASE("CBlockBase stores and enumerates named values", "[blockbase]") +{ + CBlockBase block("TestBlock", "REGION"); + + block.SetValue("Name", Value("Cadiz")); + block.SetValue("Bauern", Value(int32_t(5600))); + + CHECK(block.NumValues() == 2); + CHECK(block.GetValue("Name").asString() == "Cadiz"); + CHECK(block.GetValue("Bauern").asLong() == 5600); + CHECK(block.GetValue("missing", Value("fallback")).asString() == "fallback"); + + std::string firstName; + Value firstValue = block.GetValue(0, firstName); + CHECK_FALSE(firstName.empty()); + CHECK(firstValue.getType() != VT_EMPTY); +} + +TEST_CASE("CBlockBase finds keyed subblocks through object access", "[blockbase]") +{ + CBlockBase root("Root", "REPORT"); + auto region = std::make_shared("Region", "REGION", Value(int32_t(-16)), Value(int32_t(47))); + + region->SetValue("Name", Value("Cadiz")); + region->SetValue("Lohn", Value(int32_t(14))); + root.AddBlock(region); + + CHECK(root.NumSubblocks() == 1); + CHECK(CBlockBase::GetValue(&root, "REGION[-16,47].Name").asString() == "Cadiz"); + CHECK(CBlockBase::GetValue(&root, "REGION[-16,47].Lohn").asLong() == 14); + CHECK(CBlockBase::GetValue(&root, "REGION[-16,46].Name", Value("unknown")).asString() == "unknown"); + CHECK(CBlockBase::GetValue(&root, "REGION.size").asLong() == 1); +} + diff --git a/EBase/test/expression_tests.cpp b/EBase/test/expression_tests.cpp index c7ad53c..1bada9c 100644 --- a/EBase/test/expression_tests.cpp +++ b/EBase/test/expression_tests.cpp @@ -74,7 +74,7 @@ Value FStatusText(Expression*, ArgumentList&) { return Value(); } Value FSystem(Expression*, ArgumentList&) { return Value(); } bool DoUserFunction(const std::string&, ArgumentList&, Value*) { return false; } -std::string GetConfigFileName() { return ""; } +std::string GetConfigFileName() { return EBASE_TEST_CONFIG_FILE; } void SetConfigFileName(const std::string&) {} bool ExistUserFunction(const std::string&) { return false; } diff --git a/EBase/test/fixtures/test-v69.cr b/EBase/test/fixtures/test-v69.cr new file mode 100644 index 0000000..34cdef7 --- /dev/null +++ b/EBase/test/fixtures/test-v69.cr @@ -0,0 +1,249 @@ +VERSION 69 +"UTF-8";charset +"de";locale +1;noskillpoints +"Standard";Konfiguration +"Hex";Koordinaten +36;Basis +803;Runde +2;Zeitalter +PARTEI 49 +"de";locale +263;age +2647;Optionen +162612;Punkte +255894;Punktedurchschnitt +"Zwerge";Typ +110;Rekrutierungskosten +3264;Anzahl Personen +"draig";Magiegebiet +29;heroes +36;max_heroes +4;nmr +"Volk der Gulrowinger";Parteiname +OPTIONEN +1;AUSWERTUNG +1;COMPUTER +1;ZUGVORLAGE +1;STATISTIK +0;DEBUG +1;ZIPPED +0;ZEITUNG +1;ADRESSEN +0;BZIP2 +1;PUNKTE +0;SHOWSKCHANGE +0;XML +REGION 3 2 +"Eadrax";Name +"Ebene";Terrain +"Das Land wo Milch und Honig keine Sau interessieren...";Beschr +3228;Bauern +186;Pferde +262211;Silber +13110;Unterh +80;Rekruten +14;Lohn +65;Baeume +2;Schoesslinge +46;Eisen +RESOURCE 1910480682 +"Silber";type +262211;number +RESOURCE 350575486 +"Bauern";type +3228;number +RESOURCE 1901115851 +"Pferde";type +186;number +RESOURCE 1355696724 +"Schößlinge";type +2;number +RESOURCE 1735155814 +"Bäume";type +65;number +RESOURCE 1734599010 +"Eisen";type +11;skill +46;number +PREISE +100;Balsam +125;Gewürz +175;Juwel +110;Myrrhe +-3;Öl +150;Seide +72;Weihrauch +GRENZE 1 +"Straße";typ +1;richtung +100;prozent +GRENZE 2 +"Straße";typ +2;richtung +100;prozent +GRENZE 3 +"Straße";typ +5;richtung +100;prozent +BURG 1004152 +"Burg";Typ +"Heiliger Tempel vom Buch";Name +401;Groesse +1594019;Besitzer +49;Partei +BURG 712593 +"Sägewerk";Typ +"Sägewerk";Name +1;Groesse +BURG 876379 +"Bergwerk";Typ +"Bergwerk";Name +1;Groesse +258005;Besitzer +49;Partei +EINHEIT 1594019 +"Zwerg";Name +49;Partei +5;Anzahl +"Zwerge";Typ +1004152;Burg +4;Kampfstatus +3996148;weight +COMMANDS +"LERNEN Wahrnehmung" +TALENTE +20400 16;Wahrnehmung +GEGENSTAENDE +77;Balsam +21;Flachwurz +23;Steinbeißer +20;Spaltwachs +21;Grüner Spinnerich +33;Elfenlieb +21;Wasserfinder +118;Pferd +11;Eisen +1196848;Silber +5258;Öl +101;Stein +EINHEIT 1627763 +"Junge Zwerge";Name +49;Partei +5;Anzahl +"Zwerge";Typ +1004152;Burg +0;Kampfstatus +5000;weight +COMMANDS +"LERNEN Steinbau" +TALENTE +9900 13;Steinbau +EINHEIT 258005 +"Junge Zwerge";Name +49;Partei +1;Anzahl +"Zwerge";Typ +876379;Burg +0;Kampfstatus +48940;weight +COMMANDS +"LERNEN Bergbau" +TALENTE +2730 15;Bergbau +GEGENSTAENDE +47940;Silber +REGION 3 4 +"Antares";Name +"Wüste";Terrain +157;Bauern +63;Pferde +7090;Silber +354;Unterh +3;Rekruten +12;Lohn +24;Baeume +6;Schoesslinge +RESOURCE 1910480682 +"Silber";type +7090;number +RESOURCE 350575486 +"Bauern";type +157;number +RESOURCE 1901115851 +"Pferde";type +63;number +RESOURCE 1355696724 +"Schößlinge";type +6;number +RESOURCE 1735155814 +"Bäume";type +24;number +PREISE +100;Balsam +110;Gewürz +168;Juwel +100;Myrrhe +66;Öl +144;Seide +-4;Weihrauch +BURG 1664395 +"Befestigung";Typ +"Elfenturm";Name +"Kolchis";Beschr +30;Groesse +EINHEIT 229801 +"Junger Zwerg";Name +49;Partei +1;Anzahl +"Zwerge";Typ +4;Kampfstatus +849251;weight +COMMANDS +"// Zwerge für Kugir" +"LERNEN Bergbau" +TALENTE +1980 13;Bergbau +GEGENSTAENDE +3;Balsam +38;Wasserfinder +160;Pferd +30;Weihrauch +25;Holz +29051;Silber +1;Speer +EINHEIT 1431898 +"Junger Zwerg";Name +49;Partei +1;Anzahl +"Zwerge";Typ +4;Kampfstatus +3560;weight +COMMANDS +"LERNEN Steinbau" +TALENTE +2340 14;Steinbau +GEGENSTAENDE +2560;Silber +EINHEIT 286532 +"Junger Zwerg";Name +49;Partei +1;Anzahl +"Zwerge";Typ +4;Kampfstatus +1000;weight +COMMANDS +"LERNEN Wahrnehmung" +TALENTE +1980 11;Wahrnehmung +EINHEIT 1640624 +"Junger Zwerg";Name +49;Partei +1;Anzahl +"Zwerge";Typ +4;Kampfstatus +1000;weight +COMMANDS +"LERNEN Tarnung" +TALENTE +1650 9;Tarnung diff --git a/EBase/test/report_parse_tests.cpp b/EBase/test/report_parse_tests.cpp new file mode 100644 index 0000000..d6f96b0 --- /dev/null +++ b/EBase/test/report_parse_tests.cpp @@ -0,0 +1,82 @@ +#include +#include + +#include + +namespace { + +std::string fixturePath(const std::string& fileName) +{ + return std::string(EBASE_TEST_FIXTURE_DIR) + "/" + fileName; +} + +} // namespace + +TEST_CASE("CReport parses synthesized party 49 regions fixture", "[report]") +{ + CReport report(fixturePath("test-v69.cr")); + + REQUIRE(report.IsValid()); + CHECK(report.IsUtf8()); + CHECK(report.Version() == 69); + CHECK(report.Runde() == 803); + CHECK(report.Partei() == 49); + CHECK(report.PNrBase() == 36); + CHECK(report.BNrBase() == 36); + CHECK(report.Spiel() == "Eressea"); + CHECK(report.Konfiguration() == "Standard"); + CHECK(report.Parteiname() == "Volk der Gulrowinger"); + CHECK(report.Rekrutierungskosten() == 110); + CHECK(report.GetMap()->Regions().size() == 2); + CHECK(report.GEinheiten().size() == 7); + + CRegion* eadrax = report.GetMap()->GetFromECords(3, 2, 0); + REQUIRE(eadrax != nullptr); + CHECK(eadrax->GetName() == "Eadrax"); + CHECK(eadrax->GetEX() == 3); + CHECK(eadrax->GetEY() == 2); + CHECK(eadrax->GetEZ() == 0); + CHECK(eadrax->GetBauern() == 3228); + CHECK(eadrax->GetPferde() == 186); + CHECK(eadrax->GetSilber() == 262211); + CHECK(eadrax->GetUnterhalt() == 13110); + CHECK(eadrax->GetRekruten() == 80); + CHECK(eadrax->GetLohn() == 14); + CHECK(eadrax->NumFrontiers() == 3); + CHECK(eadrax->NumBuildings() == 3); + + REQUIRE(eadrax->GetResource("Silber") != nullptr); + CHECK(eadrax->GetResource("Silber")->GetValue("number").asLong() == 262211); + REQUIRE(eadrax->GetResource("Eisen") != nullptr); + CHECK(eadrax->GetResource("Eisen")->GetValue("skill").asLong() == 11); + CHECK(eadrax->GetResource("Eisen")->GetValue("number").asLong() == 46); + + REQUIRE(eadrax->GetBuilding(1004152) != nullptr); + CHECK(eadrax->GetBuilding(1004152)->Name() == "Heiliger Tempel vom Buch"); + REQUIRE(eadrax->GetBuilding(876379) != nullptr); + CHECK(eadrax->GetBuilding(876379)->Besitzer() == 258005); + + REQUIRE(eadrax->GetVEinheiten().size() == 3); + CEinheit* unit = eadrax->GetVEinheiten().front(); + CHECK(unit->Nummer() == 1594019); + CHECK(unit->Name() == "Zwerg"); + CHECK(unit->Partei() == 49); + CHECK(unit->Anzahl() == 5); + CHECK(unit->Typ() == "Zwerge"); + CHECK(unit->Bauwerk() == 1004152); + REQUIRE(unit->Talents().size() == 1); + CHECK(unit->Talents().front().m_sTyp == "Wahrnehmung"); + CHECK(unit->Talents().front().m_nStufe == 16); + REQUIRE(unit->Things().size() == 12); + CHECK(unit->Things().front().first == "Balsam"); + CHECK(unit->Things().front().second == 77); + + CRegion* antares = report.GetMap()->GetFromECords(3, 4, 0); + REQUIRE(antares != nullptr); + CHECK(antares->GetName() == "Antares"); + CHECK(DeUmlaut(antares->GetRegionTypeName()) == "wueste"); + CHECK(antares->NumBuildings() == 1); + REQUIRE(antares->GetVEinheiten().size() == 4); + CHECK(antares->GetVEinheiten().front()->Nummer() == 229801); + CHECK(antares->GetVEinheiten().front()->Things().size() == 7); +} diff --git a/EBase/test/reportstream_tests.cpp b/EBase/test/reportstream_tests.cpp new file mode 100644 index 0000000..885e34a --- /dev/null +++ b/EBase/test/reportstream_tests.cpp @@ -0,0 +1,102 @@ +#include +#include + +#include +#include +#include +#include + +#include "test_utils.h" + +TEST_CASE("CReportStream parses block lines", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_block", "REGION 12 -3 0 ; known region\n"); + + CReportStream stream(path); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enBLOCK); + CHECK(stream.GetValue() == "REGION"); + CHECK(stream.Data() == std::vector{12, -3, 0}); + CHECK(stream.GetComment() == " known region"); + CHECK(stream.GetLine() == 1); +} + +TEST_CASE("CReportStream parses integer data lines", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_integer", "10 -20, 30 ; pool\n"); + + CReportStream stream(path); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enINTEGER); + CHECK(stream.Data() == std::vector{10, -20, 30}); + CHECK(stream.GetComment() == " pool"); +} + +TEST_CASE("CReportStream parses and deescapes string lines", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_string", "\"quoted \\\"name\\\"\";Name\n"); + + CReportStream stream(path); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enSTRING); + CHECK(stream.GetValue() == "quoted \"name\""); + CHECK(stream.GetComment() == "Name"); +} + +TEST_CASE("CReportStream skips empty lines and supports Unget", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_unget", "\n\nVERSION 66\n\"Eressea\";Spiel\n"); + + CReportStream stream(path); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enBLOCK); + CHECK(stream.GetValue() == "VERSION"); + CHECK(stream.GetDat(0) == 66); + CHECK(stream.GetLine() == 2); + + REQUIRE(stream.Unget()); + REQUIRE_FALSE(stream.Unget()); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enBLOCK); + CHECK(stream.GetValue() == "VERSION"); + CHECK(stream.GetDat(0) == 66); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enSTRING); + CHECK(stream.GetValue() == "Eressea"); + CHECK(stream.GetComment() == "Spiel"); +} + +TEST_CASE("CReportStream strips an initial UTF-8 BOM", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_bom", std::string("\xEF\xBB\xBF") + "VERSION 66\n"); + + CReportStream stream(path); + + REQUIRE(stream.Next()); + CHECK(stream.GetType() == CReportStream::enBLOCK); + CHECK(stream.GetValue() == "VERSION"); + CHECK(stream.GetDat(0) == 66); +} + +TEST_CASE("CReportStream write helpers produce CR lines", "[reportstream]") +{ + auto path = writeTempTestFile("reportstream_write_helpers", ""); + { + CReportStream stream(path, false); + stream.WriteBlock("REGION", "known", 1, -2, 0); + stream.WriteLine("Cadiz", "Name"); + stream.WriteLine(5600, "Bauern"); + stream.WriteLine(10, 20, "Koordinaten"); + } + + std::ifstream in(path, std::ios::binary); + std::string contents((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + + CHECK(contents == "REGION 1 -2 0;known\n\"Cadiz\";Name\n5600;Bauern\n10 20;Koordinaten\n"); +} diff --git a/EBase/test/test_utils.h b/EBase/test/test_utils.h new file mode 100644 index 0000000..5012299 --- /dev/null +++ b/EBase/test/test_utils.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +class TemporaryDirectory { +public: + explicit TemporaryDirectory(std::string prefix = "tmp") + : _prefix(std::move(prefix)) + { + namespace fs = std::filesystem; + fs::path base = fs::current_path(); + std::error_code ec; + + for (int attempt = 0; attempt < 100; ++attempt) { + if (fs::path candidate = base / make_name(); fs::create_directory(candidate, ec)) { + _path = std::move(candidate); + return; + } + // if it already exists, ec will be non-zero but create_directory returns false + ec.clear(); + } + + throw std::runtime_error("TempDir: unable to create a unique temporary directory"); + } + + // non‐copyable + TemporaryDirectory(TemporaryDirectory const&) = delete; + TemporaryDirectory& operator=(TemporaryDirectory const&) = delete; + + // movable + TemporaryDirectory(TemporaryDirectory&& o) noexcept : _prefix(), _path(std::exchange(o._path, {})) {} + TemporaryDirectory& operator=(TemporaryDirectory&& o) noexcept { + if (this != &o) { + cleanup(); + _path = std::exchange(o._path, {}); + } + return *this; + } + + ~TemporaryDirectory() noexcept { + cleanup(); + } + + [[nodiscard]] const std::filesystem::path& path() const noexcept { return _path; } + operator std::filesystem::path() const noexcept { return _path; } // NOLINT(google-explicit-constructor) + +private: + std::string _prefix; + std::filesystem::path _path; + + void cleanup() const noexcept { + if (!_path.empty()) { + std::error_code ec; + std::filesystem::remove_all(_path, ec); + } + } + + [[nodiscard]] std::string make_name() const { + // e.g. "tmp-5F3A7D" + thread_local std::mt19937_64 eng{ std::random_device{}() }; + thread_local std::uniform_int_distribution dist; + + uint64_t r = dist(eng); + std::string hex; + hex.reserve(16); + for (int i = 0; i < 6; ++i) { + uint8_t nib = (r >> (i*4)) & 0xF; + hex.push_back(static_cast(nib < 10 ? '0' + nib : 'A' + (nib - 10))); + } + return _prefix + "-" + hex; + } +}; + +inline std::string writeTempTestFile(const std::string& stem, const std::string& contents) +{ + static int counter = 0; + static TemporaryDirectory tempDir("ebase-tests"); + fs::path path = tempDir.path() / (stem + "_" + std::to_string(++counter) + ".cr"); + std::ofstream out(path, std::ios::binary); + out << contents; + return path.string(); +} + +inline bool testFileExists(const std::string& path) +{ + std::ifstream in(path, std::ios::binary); + return in.good(); +} diff --git a/Vorlage/CMakeLists.txt b/Vorlage/CMakeLists.txt index a5e8628..4bd14bf 100644 --- a/Vorlage/CMakeLists.txt +++ b/Vorlage/CMakeLists.txt @@ -1,7 +1,21 @@ -SET(VORLAGE_SOURCES - #crashdumphandler.cpp +SET(METASCRIPT_SOURCES CRNE.cpp Metascript.cpp +) + +SET(METASCRIPT_HEADERS + CRNE.h + Metascript.h +) + +add_library(metascript STATIC ${METASCRIPT_SOURCES} ${METASCRIPT_HEADERS}) +target_link_libraries(metascript PUBLIC ebase) +if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) + target_compile_definitions(metascript PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +SET(VORLAGE_SOURCES + #crashdumphandler.cpp Zugvorlage.cpp ) @@ -13,17 +27,18 @@ SET(VORLAGE_HEADERS ) add_executable(vorlage ${VORLAGE_SOURCES} ${VORLAGE_HEADERS} ${CMAKE_BINARY_DIR}/${PROJECT_LOWERCASE_NAME}/version.h) -target_link_libraries(vorlage ebase) +target_link_libraries(vorlage metascript) if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) target_compile_definitions(vorlage PRIVATE _CRT_SECURE_NO_WARNINGS) #target_compile_options(vorlage PRIVATE "$<$:/utf-8>") #target_compile_options(vorlage PRIVATE "$<$:/utf-8>") endif() +add_subdirectory(test) + if(APPLE AND NOT CMAKE_BUILD_TYPE STREQUAL "Release") add_custom_command(TARGET vorlage POST_BUILD COMMAND codesign -f -s - --entitlements ${CMAKE_SOURCE_DIR}/cmake/debug.entitlements $ COMMENT "Signing vorlage with debug entitlements" ) endif() - diff --git a/Vorlage/test/CMakeLists.txt b/Vorlage/test/CMakeLists.txt new file mode 100644 index 0000000..d361bcb --- /dev/null +++ b/Vorlage/test/CMakeLists.txt @@ -0,0 +1,22 @@ +add_executable(metascript_catch_tests + metascript_tests.cpp +) + +target_link_libraries(metascript_catch_tests metascript Catch2::Catch2WithMain) +target_compile_definitions(metascript_catch_tests PRIVATE + METASCRIPT_TEST_CONFIG_FILE="${CMAKE_SOURCE_DIR}/config/eressea.cfg" +) + +if(CMAKE_CXX_COMPILER_ID MATCHES MSVC) + target_compile_definitions(metascript_catch_tests PRIVATE _CRT_SECURE_NO_WARNINGS) + target_compile_options(metascript_catch_tests PRIVATE "$<$:/utf-8>") + target_compile_options(metascript_catch_tests PRIVATE "$<$:/utf-8>") +endif() + +if(APPLE) + target_compile_options(metascript_catch_tests PRIVATE -mmacosx-version-min=10.15) + target_link_options(metascript_catch_tests PRIVATE -mmacosx-version-min=10.15) +endif() + +include(Catch) +catch_discover_tests(metascript_catch_tests) diff --git a/Vorlage/test/metascript_tests.cpp b/Vorlage/test/metascript_tests.cpp new file mode 100644 index 0000000..e72a762 --- /dev/null +++ b/Vorlage/test/metascript_tests.cpp @@ -0,0 +1,140 @@ +#include + +#include +#include +#include + +#include +#include + +int32_t g_nTimeCorrection = 0; + +std::string GetConfigFileName() +{ + return METASCRIPT_TEST_CONFIG_FILE; +} + +void SetConfigFileName(const std::string&) +{ +} + +namespace { + +struct MetaScriptFixture { + MetaScriptFixture() + { + Expression::clearAllVars(); + g_coFlags.insert(VF_NOCONSOLE); + CMetaCommand::SetTrace(0); + g_nLimitRuntime = 0; + g_nTimeCorrection = 0; + } +}; + +VKommandos runInline(const std::string& script) +{ + CMetaCommand command(script, 1); + command.SetFile(""); + + CMCI mci(0); + Expression::Variables vars; + VKommandos output; + std::string persisted = script; + + command.RunScript(mci, vars, &persisted, output); + return output; +} + +std::string commandAt(const VKommandos& commands, int32_t index) +{ + return commands[index].asString(); +} + +} // namespace + +TEST_CASE_METHOD(MetaScriptFixture, "CMetaCommand executes interpreter-only conditionals", "[metascript]") +{ + VKommandos output = runInline("#if 1 { #message 'yes' } #else { #message 'no' }"); + + REQUIRE(output.size() == 1); + CHECK(commandAt(output, 0) == "; yes"); +} + +TEST_CASE_METHOD(MetaScriptFixture, "CScriptBase imports procedures without a Vorlage instance", "[metascript]") +{ + CScriptBase scripts; + + REQUIRE(scripts.Import("", + R"( +#proc LocalImported $text +{ + #message $text +} +)")); + + CMetaCommand* proc = scripts.FindProc("LocalImported"); + REQUIRE(proc != nullptr); + CHECK_FALSE(proc->IsFunction()); + CHECK(proc->Args() >= 5); +} + +TEST_CASE_METHOD(MetaScriptFixture, "global script base calls imported procedures", "[metascript]") +{ + REQUIRE(g_oScriptBase.Import("", + R"( +#proc GlobalProcedure +{ + #message 'from proc' +} +)")); + + VKommandos output; + REQUIRE(CMetaCommand::Call("GlobalProcedure", output)); + + REQUIRE(output.size() == 1); + CHECK(commandAt(output, 0) == "; from proc"); +} + +TEST_CASE_METHOD(MetaScriptFixture, "metascript functions are available to expressions", "[metascript]") +{ + REQUIRE(g_oScriptBase.Import("", + R"( +#func AddValues $left $right +{ + #return $left+$right +} +)")); + + ArgumentList args; + args.push_back(Value(2)); + args.push_back(Value(5)); + + Value result; + REQUIRE(DoUserFunction("AddValues", args, &result)); + CHECK(result.asLong() == 7); +} + +TEST_CASE_METHOD(MetaScriptFixture, "procedures can use local variables and loops", "[metascript]") +{ + REQUIRE(g_oScriptBase.Import("", + R"( +#proc LoopProcedure +{ + #var $i + $i=0 + #while $i<3 + { + #message $i + $i=$i+1 + } +} +)")); + + VKommandos output; + REQUIRE(CMetaCommand::Call("LoopProcedure", output)); + + REQUIRE(output.size() == 3); + CHECK(commandAt(output, 0) == "; 0"); + CHECK(commandAt(output, 1) == "; 1"); + CHECK(commandAt(output, 2) == "; 2"); +} From 9d8f9164b4552e8aa88b9680fa1f41bda2ab593c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Schu=CC=88mann?= Date: Tue, 28 Apr 2026 17:22:41 +0200 Subject: [PATCH 2/2] Normalize line endings in `reportstream_tests.cpp` to ensure consistent test output --- EBase/test/reportstream_tests.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/EBase/test/reportstream_tests.cpp b/EBase/test/reportstream_tests.cpp index 885e34a..4722854 100644 --- a/EBase/test/reportstream_tests.cpp +++ b/EBase/test/reportstream_tests.cpp @@ -8,6 +8,30 @@ #include "test_utils.h" +namespace { + +std::string normalizeLineEndings(std::string text) +{ + std::string normalized; + normalized.reserve(text.size()); + + for (size_t i = 0; i < text.size(); ++i) { + if (text[i] == '\r') { + if (i + 1 < text.size() && text[i + 1] == '\n') { + ++i; + } + normalized.push_back('\n'); + } + else { + normalized.push_back(text[i]); + } + } + + return normalized; +} + +} // namespace + TEST_CASE("CReportStream parses block lines", "[reportstream]") { auto path = writeTempTestFile("reportstream_block", "REGION 12 -3 0 ; known region\n"); @@ -98,5 +122,5 @@ TEST_CASE("CReportStream write helpers produce CR lines", "[reportstream]") std::ifstream in(path, std::ios::binary); std::string contents((std::istreambuf_iterator(in)), std::istreambuf_iterator()); - CHECK(contents == "REGION 1 -2 0;known\n\"Cadiz\";Name\n5600;Bauern\n10 20;Koordinaten\n"); + CHECK(normalizeLineEndings(contents) == "REGION 1 -2 0;known\n\"Cadiz\";Name\n5600;Bauern\n10 20;Koordinaten\n"); }