Skip to content

Commit 8ef755d

Browse files
committed
tests: add unit-tests for gc
1 parent 9a39e49 commit 8ef755d

4 files changed

Lines changed: 428 additions & 0 deletions

File tree

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ add_executable(
1212
bytecode_parser_tests.cpp
1313
bytecode_commands_tests.cpp
1414
builtin_functions_tests.cpp
15+
gc_tests.cpp
16+
test_suites/GcTestSuite.cpp
1517
)
1618

1719
target_link_libraries(

tests/gc_tests.cpp

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#include <gtest/gtest.h>
2+
3+
#include "tests/test_suites/GcTestSuite.hpp"
4+
5+
#include <memory>
6+
#include <stdexcept>
7+
#include <unordered_set>
8+
#include <vector>
9+
10+
#include "lib/execution_tree/ExecutionResult.hpp"
11+
#include "lib/execution_tree/FunctionRepository.hpp"
12+
#include "lib/execution_tree/PassedExecutionData.hpp"
13+
#include "lib/runtime/MemoryManager.hpp"
14+
#include "lib/runtime/ObjectDescriptor.hpp"
15+
#include "lib/runtime/ObjectRepository.hpp"
16+
#include "lib/runtime/VirtualTable.hpp"
17+
#include "lib/runtime/VirtualTableRepository.hpp"
18+
#include "lib/runtime/gc/MarkAndSweepGC.hpp"
19+
#include "lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp"
20+
#include "lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp"
21+
22+
TEST_F(GcTestSuite, UnreachableObjectCollected) {
23+
auto data = MakeFreshData();
24+
25+
void* obj = AllocateTestObject("Simple", data);
26+
27+
auto before = SnapshotRepo(mm_.GetRepository());
28+
ASSERT_EQ(before.size(), 1u);
29+
ASSERT_TRUE(RepoContains(mm_.GetRepository(), obj));
30+
31+
CollectGarbage(data);
32+
33+
auto after = SnapshotRepo(mm_.GetRepository());
34+
EXPECT_EQ(after.size(), 0u);
35+
EXPECT_FALSE(RepoContains(mm_.GetRepository(), obj));
36+
}
37+
38+
TEST_F(GcTestSuite, RootInGlobalVariablesSurvives) {
39+
auto data = MakeFreshData();
40+
41+
void* obj = AllocateTestObject("Simple", data);
42+
data.memory.global_variables.emplace_back(obj);
43+
44+
CollectGarbage(data);
45+
46+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj));
47+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 1u);
48+
}
49+
50+
TEST_F(GcTestSuite, RootInMachineStackSurvives) {
51+
auto data = MakeFreshData();
52+
53+
void* obj = AllocateTestObject("Simple", data);
54+
data.memory.machine_stack.emplace(obj);
55+
56+
CollectGarbage(data);
57+
58+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj));
59+
}
60+
61+
TEST_F(GcTestSuite, TransitiveReachabilityThroughField) {
62+
auto data = MakeFreshData();
63+
64+
void* root = AllocateTestObject("WithRef", data);
65+
void* child = AllocateTestObject("Simple", data);
66+
67+
SetRef(root, child);
68+
69+
data.memory.global_variables.emplace_back(root);
70+
71+
CollectGarbage(data);
72+
73+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), root));
74+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), child));
75+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 2u);
76+
}
77+
78+
TEST_F(GcTestSuite, TransitiveReachabilityThroughArray) {
79+
auto data = MakeFreshData();
80+
81+
void* arr = AllocateTestObject("Array", data);
82+
InitArray(arr);
83+
84+
void* child1 = AllocateTestObject("Simple", data);
85+
void* child2 = AllocateTestObject("Simple", data);
86+
87+
AddToArray(arr, child1);
88+
AddToArray(arr, child2);
89+
90+
data.memory.global_variables.emplace_back(arr);
91+
92+
CollectGarbage(data);
93+
94+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), arr));
95+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), child1));
96+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), child2));
97+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 3u);
98+
}
99+
100+
TEST_F(GcTestSuite, CycleWithoutRootsCollected) {
101+
auto data = MakeFreshData();
102+
103+
void* objA = AllocateTestObject("WithRef", data);
104+
void* objB = AllocateTestObject("WithRef", data);
105+
106+
SetRef(objA, objB);
107+
SetRef(objB, objA);
108+
109+
CollectGarbage(data);
110+
111+
EXPECT_FALSE(RepoContains(mm_.GetRepository(), objA));
112+
EXPECT_FALSE(RepoContains(mm_.GetRepository(), objB));
113+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 0u);
114+
}
115+
116+
TEST_F(GcTestSuite, CycleWithRootPreserved) {
117+
auto data = MakeFreshData();
118+
119+
void* objA = AllocateTestObject("WithRef", data);
120+
void* objB = AllocateTestObject("WithRef", data);
121+
122+
SetRef(objA, objB);
123+
SetRef(objB, objA);
124+
125+
data.memory.global_variables.emplace_back(objA);
126+
127+
CollectGarbage(data);
128+
129+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), objA));
130+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), objB));
131+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 2u);
132+
}
133+
134+
TEST_F(GcTestSuite, MultipleRootsDifferentPlaces) {
135+
auto data = MakeFreshData();
136+
137+
void* global = AllocateTestObject("Simple", data);
138+
void* stack = AllocateTestObject("Simple", data);
139+
void* local = AllocateTestObject("Simple", data);
140+
141+
data.memory.global_variables.emplace_back(global);
142+
data.memory.machine_stack.emplace(stack);
143+
144+
ovum::vm::runtime::StackFrame frame;
145+
frame.local_variables.emplace_back(local);
146+
data.memory.stack_frames.push(std::move(frame));
147+
148+
CollectGarbage(data);
149+
150+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), global));
151+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), stack));
152+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), local));
153+
EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 3u);
154+
}
155+
156+
TEST_F(GcTestSuite, SmallThresholdFrequentAllocations) {
157+
auto data = MakeFreshData(3);
158+
159+
std::vector<void*> objects;
160+
std::vector<void*> should_survive;
161+
162+
const int k_iterations = 15;
163+
164+
for (int i = 0; i < k_iterations; ++i) {
165+
void* obj = AllocateTestObject("Simple", data);
166+
objects.push_back(obj);
167+
168+
if (i % 3 == 0) {
169+
data.memory.global_variables.emplace_back(obj);
170+
should_survive.push_back(obj);
171+
}
172+
173+
auto gc_res = data.memory_manager.CollectGarbageIfRequired(data);
174+
ASSERT_TRUE(gc_res.has_value());
175+
}
176+
177+
CollectGarbage(data);
178+
179+
auto snapshot = SnapshotRepo(mm_.GetRepository());
180+
EXPECT_GE(snapshot.size(), should_survive.size());
181+
182+
for (void* obj : should_survive) {
183+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj));
184+
}
185+
}
186+
187+
TEST_F(GcTestSuite, NullReferencesNotCrashing) {
188+
auto data = MakeFreshData();
189+
190+
void* root = AllocateTestObject("WithRef", data);
191+
SetRef(root, nullptr);
192+
193+
data.memory.global_variables.emplace_back(root);
194+
195+
ASSERT_NO_FATAL_FAILURE(CollectGarbage(data));
196+
197+
EXPECT_TRUE(RepoContains(mm_.GetRepository(), root));
198+
}

tests/test_suites/GcTestSuite.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#include "GcTestSuite.hpp"
2+
#include "lib/execution_tree/Block.hpp"
3+
#include "lib/execution_tree/Command.hpp"
4+
#include "lib/execution_tree/Function.hpp"
5+
#include "lib/execution_tree/PureFunction.hpp"
6+
#include "lib/executor/builtin_factory.hpp"
7+
8+
#include <cassert>
9+
10+
GcTestSuite::GcTestSuite() : vtr_(), fr_(), mm_(std::make_unique<ovum::vm::runtime::MarkAndSweepGC>(), kDefaultGCThreshold), rm_() {
11+
}
12+
13+
ovum::vm::execution_tree::PassedExecutionData GcTestSuite::MakeFreshData(uint64_t gc_threshold) {
14+
mm_ = ovum::vm::runtime::MemoryManager(std::make_unique<ovum::vm::runtime::MarkAndSweepGC>(), gc_threshold);
15+
ovum::vm::execution_tree::PassedExecutionData data{.memory = rm_,
16+
.virtual_table_repository = vtr_,
17+
.function_repository = fr_,
18+
.memory_manager = mm_,
19+
.input_stream = std::cin,
20+
.output_stream = std::cout,
21+
.error_stream = std::cerr};
22+
return data;
23+
}
24+
25+
ovum::vm::execution_tree::ExecutionResult NoOpDestructor(ovum::vm::execution_tree::PassedExecutionData& /*data*/) {
26+
return ovum::vm::execution_tree::ExecutionResult::kNormal;
27+
}
28+
29+
void GcTestSuite::SetUp() {
30+
vtr_ = ovum::vm::runtime::VirtualTableRepository();
31+
32+
fr_ = ovum::vm::execution_tree::FunctionRepository();
33+
34+
vtr_ = ovum::vm::runtime::VirtualTableRepository();
35+
36+
fr_ = ovum::vm::execution_tree::FunctionRepository();
37+
38+
{
39+
auto vt_res = ovum::vm::runtime::RegisterBuiltinVirtualTables(vtr_);
40+
ASSERT_TRUE(vt_res.has_value()) << "Failed builtin vtables: " << vt_res.error().what();
41+
}
42+
43+
{
44+
auto fn_res = ovum::vm::execution_tree::RegisterBuiltinFunctions(fr_);
45+
ASSERT_TRUE(fn_res.has_value()) << "Failed builtin functions: " << fn_res.error().what();
46+
}
47+
48+
RegisterTestVtables();
49+
50+
RegisterNoOpDestructors();
51+
52+
auto gc = std::make_unique<ovum::vm::runtime::MarkAndSweepGC>();
53+
mm_ = ovum::vm::runtime::MemoryManager(std::move(gc), 10);
54+
}
55+
56+
void GcTestSuite::RegisterTestVtables() {
57+
// Simple
58+
{
59+
ovum::vm::runtime::VirtualTable vt("Simple", sizeof(ovum::vm::runtime::ObjectDescriptor));
60+
vt.AddFunction("_destructor_<M>", "_Simple_destructor_<M>");
61+
auto res = vtr_.Add(std::move(vt));
62+
ASSERT_TRUE(res.has_value()) << "Failed Simple: " << res.error().what();
63+
}
64+
65+
// WithRef
66+
{
67+
ovum::vm::runtime::VirtualTable vt("WithRef", sizeof(ovum::vm::runtime::ObjectDescriptor) + sizeof(void*));
68+
vt.AddFunction("_destructor_<M>", "_WithRef_destructor_<M>");
69+
vt.AddField("Object", sizeof(ovum::vm::runtime::ObjectDescriptor));
70+
71+
auto res = vtr_.Add(std::move(vt));
72+
ASSERT_TRUE(res.has_value());
73+
}
74+
75+
// Array
76+
{
77+
auto scanner = std::make_unique<ovum::vm::runtime::ArrayReferenceScanner>();
78+
ovum::vm::runtime::VirtualTable vt(
79+
"Array", sizeof(ovum::vm::runtime::ObjectDescriptor) + sizeof(std::vector<void*>), std::move(scanner));
80+
vt.AddFunction("_destructor_<M>", "_Array_destructor_<M>");
81+
auto res = vtr_.Add(std::move(vt));
82+
ASSERT_TRUE(res.has_value());
83+
}
84+
}
85+
86+
void GcTestSuite::RegisterNoOpDestructors() {
87+
const std::vector<std::string> types = {"Simple", "WithRef", "Array"};
88+
89+
for (const auto& type : types) {
90+
std::string func_name = "_" + type + "_destructor_<M>";
91+
92+
// Создаём простое тело — блок с одной командой (no-op)
93+
auto body = std::make_unique<ovum::vm::execution_tree::Block>();
94+
95+
auto no_op_cmd = [](ovum::vm::execution_tree::PassedExecutionData& d)
96+
-> std::expected<ovum::vm::execution_tree::ExecutionResult, std::runtime_error> { return NoOpDestructor(d); };
97+
98+
body->AddStatement(std::make_unique<ovum::vm::execution_tree::Command<decltype(no_op_cmd)>>(no_op_cmd));
99+
100+
auto function = std::make_unique<ovum::vm::execution_tree::Function>(func_name, 1u, std::move(body));
101+
102+
auto res = fr_.Add(std::move(function));
103+
104+
ASSERT_TRUE(res.has_value()) << "Failed to register destructor for " << type << ": " << res.error().what();
105+
}
106+
}
107+
108+
std::unordered_set<void*> GcTestSuite::SnapshotRepo(const ovum::vm::runtime::ObjectRepository& repo) const {
109+
std::unordered_set<void*> snapshot;
110+
repo.ForAll([&snapshot](void* obj) { snapshot.insert(obj); });
111+
return snapshot;
112+
}
113+
114+
std::string GcTestSuite::TypeOf(void* obj, const ovum::vm::execution_tree::PassedExecutionData& data) const {
115+
if (!obj)
116+
return "<null>";
117+
auto* desc = reinterpret_cast<ovum::vm::runtime::ObjectDescriptor*>(obj);
118+
auto vt_res = data.virtual_table_repository.GetByIndex(desc->vtable_index);
119+
if (!vt_res.has_value())
120+
return "<invalid vtable>";
121+
return vt_res.value()->GetName();
122+
}
123+
124+
bool GcTestSuite::RepoContains(const ovum::vm::runtime::ObjectRepository& repo, void* obj) const {
125+
bool found = false;
126+
repo.ForAll([&](void* o) {
127+
if (o == obj)
128+
found = true;
129+
});
130+
return found;
131+
}
132+
133+
void* GcTestSuite::AllocateTestObject(const std::string& type_name,
134+
ovum::vm::execution_tree::PassedExecutionData& data) {
135+
auto vt_res = data.virtual_table_repository.GetByName(type_name);
136+
EXPECT_TRUE(vt_res.has_value()) << "VTable not found: " << type_name;
137+
138+
auto idx_res = data.virtual_table_repository.GetIndexByName(type_name);
139+
EXPECT_TRUE(idx_res.has_value());
140+
141+
auto alloc_res = data.memory_manager.AllocateObject(*vt_res.value(), static_cast<uint32_t>(*idx_res), data);
142+
143+
EXPECT_TRUE(alloc_res.has_value()) << "Allocation failed for " << type_name;
144+
return alloc_res.value();
145+
}
146+
147+
void GcTestSuite::SetRef(void* obj, void* target) {
148+
void** ptr = reinterpret_cast<void**>(reinterpret_cast<char*>(obj) + sizeof(ovum::vm::runtime::ObjectDescriptor));
149+
*ptr = target;
150+
}
151+
152+
void GcTestSuite::InitArray(void* array_obj) {
153+
auto* vec = reinterpret_cast<std::vector<void*>*>(reinterpret_cast<char*>(array_obj) +
154+
sizeof(ovum::vm::runtime::ObjectDescriptor));
155+
new (vec) std::vector<void*>();
156+
}
157+
158+
void GcTestSuite::AddToArray(void* array_obj, void* item) {
159+
auto* vec = reinterpret_cast<std::vector<void*>*>(reinterpret_cast<char*>(array_obj) +
160+
sizeof(ovum::vm::runtime::ObjectDescriptor));
161+
vec->push_back(item);
162+
}
163+
164+
void GcTestSuite::CollectGarbage(ovum::vm::execution_tree::PassedExecutionData& data) {
165+
auto res = data.memory_manager.CollectGarbage(data);
166+
ASSERT_TRUE(res.has_value()) << "GC failed: " << res.error().what();
167+
}

0 commit comments

Comments
 (0)