From 67b1b52301f49c33d71a6928b9e1d431ee265f8c Mon Sep 17 00:00:00 2001 From: metaq3 Date: Fri, 19 Jun 2026 02:18:37 +0500 Subject: [PATCH] fix: smart memory managing against memory corruption when tests shuffle Some tests are divided into groups that share same memory chunk. They rely on it and assume it's never touched by other tests. Catch2 may shuffle tests from different groups with each other to ensure they're state independent. This causes them to corrupt memory of each other. It was fixed with memory forks. Each test group contains own fork of zone memory, and this fork is activated every time test needs one. --- code/qcommon/common.c | 2 +- code/qcommon/qcommon.h | 1 + code/tests/memory.cxx | 87 ++++++++++++++++++++ code/tests/memory.hxx | 20 +++++ code/tests/test_cg_superhud_configparser.cxx | 19 +++-- code/tests/test_osp_drawstring.cxx | 13 ++- code/tests/test_qcommon_zone_allocator.cxx | 11 ++- 7 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 code/tests/memory.cxx create mode 100644 code/tests/memory.hxx diff --git a/code/qcommon/common.c b/code/qcommon/common.c index 7236f25f..23dcd372 100644 --- a/code/qcommon/common.c +++ b/code/qcommon/common.c @@ -71,7 +71,7 @@ static int minfragment = MINFRAGMENT; // may be adjusted at runtime static memzone_t *mainzone; static memzone_t *smallzone; -static int mainzone_static[MAINZONE_STATIC_SIZE/sizeof(int)]; //static mainzone memory +int mainzone_static[MAINZONE_STATIC_SIZE/sizeof(int)]; //static mainzone memory // static const char *tagName[ TAG_COUNT ] = { "FREE", diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index 3f56c21f..f3691b1f 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -6,6 +6,7 @@ extern "C" { #endif +extern int mainzone_static[]; typedef struct zone_stats_s { int zoneSegments; diff --git a/code/tests/memory.cxx b/code/tests/memory.cxx new file mode 100644 index 00000000..adb89222 --- /dev/null +++ b/code/tests/memory.cxx @@ -0,0 +1,87 @@ +#include "memory.hxx" +#include "../qcommon/qcommon.h" +#include +#include +#include +#include + +namespace Memory { + +namespace Constants { + +static constexpr std::size_t kMemorySize = MAINZONE_STATIC_SIZE; +static constexpr int *kMemory = &mainzone_static[0]; + +}; // namespace Constants + +static std::shared_ptr currentFork = nullptr; +static std::vector> forks; + +}; // namespace Memory + +/** + * @brief Dumps current fork to memory + */ +static void DumpCurrentFork() { + if (!Memory::currentFork) + return; + + const int *currentForkBegin = Memory::currentFork.get(); + const int *currentForkEnd = Memory::currentFork.get() + + (Memory::Constants::kMemorySize / sizeof(int)); + + std::copy(currentForkBegin, currentForkEnd, Memory::Constants::kMemory); +} + +/** + * @brief Loads memory content to current fork + */ +static void LoadToCurrentFork() { + if (!Memory::currentFork) + return; + + const int *memoryBegin = Memory::Constants::kMemory; + const int *memoryEnd = Memory::Constants::kMemory + + (Memory::Constants::kMemorySize / sizeof(int)); + + std::copy(memoryBegin, memoryEnd, Memory::currentFork.get()); +} + +/** + * @brief Allocates and initializes new fork. + * + * @return int* - fork + */ +static std::shared_ptr ForkFromMainzone() { + std::shared_ptr fork( + new int[Memory::Constants::kMemorySize / sizeof(int)]); + + std::copy(Memory::Constants::kMemory, + Memory::Constants::kMemory + (MAINZONE_STATIC_SIZE / sizeof(int)), + fork.get()); + + return fork; +} + +std::size_t ForkMemory() { + // Syncronize previous fork with memory + LoadToCurrentFork(); + + Memory::currentFork = ForkFromMainzone(); + Memory::forks.push_back(Memory::currentFork); + + return Memory::forks.size() - 1; +} + +void SwitchFork(std::size_t id) { + if (Memory::currentFork == Memory::forks.at(id)) + return; + + // Syncronize previous fork with memory + LoadToCurrentFork(); + + Memory::currentFork = Memory::forks[id]; + + // Dump newly loaded fork into memory + DumpCurrentFork(); +} diff --git a/code/tests/memory.hxx b/code/tests/memory.hxx new file mode 100644 index 00000000..01d3fcca --- /dev/null +++ b/code/tests/memory.hxx @@ -0,0 +1,20 @@ +#pragma once +#include + +/** + * @brief Creates an independent copy of the current zone memory state and + * returns a unique fork ID. The new fork starts as an exact replica but + * evolves separately. + * + * @note Used in tests. Test group may share memory, but tests may be shuffled + * and we don't want them to corrupt each other's memory. + * + * @return std::size_t + */ +std::size_t ForkMemory(); + +/** + * @brief Activates the specified fork, making its memory state the current + * working context for subsequent operations. + */ +void SwitchFork(std::size_t id); diff --git a/code/tests/test_cg_superhud_configparser.cxx b/code/tests/test_cg_superhud_configparser.cxx index e163cbb1..1dd8056f 100644 --- a/code/tests/test_cg_superhud_configparser.cxx +++ b/code/tests/test_cg_superhud_configparser.cxx @@ -2,18 +2,19 @@ #include #include "../cgame/cg_superhud_private.h" #include "../qcommon/qcommon.h" +#include "memory.hxx" -namespace +static void shud_test_init() { - bool initialized = false; - void shud_test_init() - { - if (initialized) return; - Com_InitZoneMemory(); - CG_SHUDParserInit(); - initialized = true; - } + static bool initialized = false; + static std::size_t forkId = ForkMemory(); + + SwitchFork(forkId); + if (initialized) return; + Com_InitZoneMemory(); + CG_SHUDParserInit(); + initialized = true; } diff --git a/code/tests/test_osp_drawstring.cxx b/code/tests/test_osp_drawstring.cxx index 84730b88..5738e7fb 100644 --- a/code/tests/test_osp_drawstring.cxx +++ b/code/tests/test_osp_drawstring.cxx @@ -3,12 +3,19 @@ #include "../cgame/cg_local.h" #include "../qcommon/qcommon.h" +#include "memory.hxx" +static void InitZoneMemory() { + static std::size_t forkId = ForkMemory(); + + SwitchFork(forkId); + Com_InitZoneMemory(); +} TEST_CASE("Prepare string", "[cgame][cg_drawtools.c]") { char out[1024]; - Com_InitZoneMemory(); + InitZoneMemory(); CG_OSPDrawStringPrepare("text", out, 1024); CHECK(strncmp("text", out, 4) == 0); @@ -51,7 +58,7 @@ TEST_CASE("Prepare string", "[cgame][cg_drawtools.c]") TEST_CASE("Text compiler", "[API][cg_drawtools.c]") { - Com_InitZoneMemory(); + InitZoneMemory(); CHECK(CG_CompileText(NULL, 0) == NULL); CHECK(CG_CompileText("", 0) == NULL); @@ -332,7 +339,7 @@ TEST_CASE("Text compiler", "[API][cg_drawtools.c]") TEST_CASE("Text compiler: bug ^^0 ", "[API][cg_drawtools.c]") { - Com_InitZoneMemory(); + InitZoneMemory(); CHECK(CG_CompileText(NULL, 0) == NULL); CHECK(CG_CompileText("", 0) == NULL); diff --git a/code/tests/test_qcommon_zone_allocator.cxx b/code/tests/test_qcommon_zone_allocator.cxx index 7318ed45..96349bc1 100644 --- a/code/tests/test_qcommon_zone_allocator.cxx +++ b/code/tests/test_qcommon_zone_allocator.cxx @@ -1,11 +1,18 @@ #include #include "../qcommon/qcommon.h" +#include "memory.hxx" +static void InitZoneMemory() { + static std::size_t forkId = ForkMemory(); + + SwitchFork(forkId); + Com_InitZoneMemory(); +} TEST_CASE("Test memory allocator", "[qcommon][memory][zone_allocator]") { - Com_InitZoneMemory(); + InitZoneMemory(); void *ptr = Z_Malloc(100); @@ -16,7 +23,7 @@ TEST_CASE("Test memory allocator", "[qcommon][memory][zone_allocator]") TEST_CASE("Test memory allocator, overflow", "[qcommon][memory][zone_allocator]") { - Com_InitZoneMemory(); + InitZoneMemory(); // too big void *ptr1 = Z_Malloc(MAINZONE_STATIC_SIZE);