rexglue-skate3/tests/unit/kernel/object_table_test.cpp
2026-05-18 22:41:13 +01:00

581 lines
15 KiB
C++

/**
* Unit tests for kernel object system (XObject and ObjectTable)
*
* Tests handle management, reference counting, and name mapping.
*/
#include <catch2/catch_test_macros.hpp>
#include <rex/logging.h>
#include <rex/system/util/object_table.h>
#include <rex/system/xobject.h>
using namespace rex;
using namespace rex::system;
using namespace rex::system::util;
// =============================================================================
// Test Fixtures and Helpers
// =============================================================================
// Minimal XObject subclass for testing
class TestObject : public XObject {
public:
static constexpr Type kObjectType = Type::Undefined;
TestObject() : XObject(Type::Undefined) {}
// Track destructor calls for leak detection
static int destructor_count;
~TestObject() override { destructor_count++; }
};
int TestObject::destructor_count = 0;
// Reset destructor count before each test
struct DestructorCountReset {
DestructorCountReset() { TestObject::destructor_count = 0; }
};
// Initialize logging once
void InitTestLogging() {
static bool initialized = false;
if (!initialized) {
rex::InitLogging();
initialized = true;
}
}
// =============================================================================
// object_ref Smart Pointer Tests
// =============================================================================
TEST_CASE("object_ref default construction is null", "[kernel][object_ref]") {
InitTestLogging();
object_ref<TestObject> ref;
CHECK(ref.get() == nullptr);
CHECK(!ref);
CHECK(ref == nullptr);
}
TEST_CASE("object_ref construction from raw pointer", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
// Object starts with ref count of 1 from constructor
{
object_ref<TestObject> ref(obj);
CHECK(ref.get() == obj);
CHECK(ref);
CHECK(ref != nullptr);
}
// Destructor should release, destroying object
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref copy construction retains", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
{
object_ref<TestObject> ref1(obj);
{
object_ref<TestObject> ref2(ref1); // Copy - should retain
CHECK(ref1.get() == ref2.get());
CHECK(TestObject::destructor_count == 0); // Still alive
}
// ref2 destroyed, but ref1 still holds
CHECK(TestObject::destructor_count == 0);
}
// Both refs destroyed
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref move construction transfers ownership", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
{
object_ref<TestObject> ref1(obj);
object_ref<TestObject> ref2(std::move(ref1));
CHECK(ref1.get() == nullptr); // Moved from
CHECK(ref2.get() == obj);
CHECK(TestObject::destructor_count == 0);
}
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref copy assignment retains", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
{
object_ref<TestObject> ref1(obj);
object_ref<TestObject> ref2;
ref2 = ref1; // Copy assign
CHECK(ref1.get() == ref2.get());
CHECK(TestObject::destructor_count == 0);
}
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref move assignment transfers ownership", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
{
object_ref<TestObject> ref1(obj);
object_ref<TestObject> ref2;
ref2 = std::move(ref1);
CHECK(ref1.get() == nullptr);
CHECK(ref2.get() == obj);
}
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref reset releases reference", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
object_ref<TestObject> ref(obj);
CHECK(TestObject::destructor_count == 0);
ref.reset();
CHECK(ref.get() == nullptr);
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref release returns pointer without destroying", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
object_ref<TestObject> ref(obj);
auto* released = ref.release();
CHECK(released == obj);
CHECK(ref.get() == nullptr);
CHECK(TestObject::destructor_count == 0); // Not destroyed
// Clean up manually
released->Release();
CHECK(TestObject::destructor_count == 1);
}
TEST_CASE("object_ref arrow operator works", "[kernel][object_ref]") {
InitTestLogging();
auto* obj = new TestObject();
object_ref<TestObject> ref(obj);
CHECK(ref->type() == XObject::Type::Undefined);
}
TEST_CASE("object_ref dereference operator works", "[kernel][object_ref]") {
InitTestLogging();
auto* obj = new TestObject();
object_ref<TestObject> ref(obj);
TestObject& deref = *ref;
CHECK(&deref == obj);
}
TEST_CASE("retain_object helper retains and wraps", "[kernel][object_ref]") {
InitTestLogging();
DestructorCountReset reset;
auto* obj = new TestObject();
// obj has ref count 1
{
auto ref = retain_object(obj); // Should retain, ref count now 2
CHECK(ref.get() == obj);
}
// ref destroyed, ref count back to 1
CHECK(TestObject::destructor_count == 0);
obj->Release(); // Final release
CHECK(TestObject::destructor_count == 1);
}
// =============================================================================
// ObjectTable Handle Allocation Tests
// =============================================================================
TEST_CASE("ObjectTable AddHandle allocates valid handle", "[kernel][object_table]") {
InitTestLogging();
DestructorCountReset reset;
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
X_STATUS status = table.AddHandle(obj, &handle);
CHECK(status == X_STATUS_SUCCESS);
CHECK(handle != 0);
CHECK(handle >= XObject::kHandleBase); // 0xF8000000
// Handle should be in object's handle list
CHECK(obj->handles().size() == 1);
CHECK(obj->handles()[0] == handle);
table.Reset();
}
TEST_CASE("ObjectTable first handle is slot 1 not 0", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
// First handle should be slot 1: kHandleBase + (1 << 2) = 0xF8000004
CHECK(handle == XObject::kHandleBase + 4);
table.Reset();
}
TEST_CASE("ObjectTable multiple objects get unique handles", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj1 = new TestObject();
auto* obj2 = new TestObject();
auto* obj3 = new TestObject();
X_HANDLE h1 = 0, h2 = 0, h3 = 0;
table.AddHandle(obj1, &h1);
table.AddHandle(obj2, &h2);
table.AddHandle(obj3, &h3);
CHECK(h1 != h2);
CHECK(h2 != h3);
CHECK(h1 != h3);
// Handles increment by 4 (slot << 2)
CHECK(h2 == h1 + 4);
CHECK(h3 == h2 + 4);
table.Reset();
}
// =============================================================================
// ObjectTable Handle Lookup Tests
// =============================================================================
TEST_CASE("ObjectTable LookupObject finds object by handle", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
auto found = table.LookupObject<XObject>(handle);
CHECK(found.get() == obj);
table.Reset();
}
TEST_CASE("ObjectTable LookupObject returns null for invalid handle", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto found = table.LookupObject<XObject>(0xF8001234);
CHECK(found.get() == nullptr);
}
TEST_CASE("ObjectTable LookupObject returns null for handle 0", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto found = table.LookupObject<XObject>(0);
CHECK(found.get() == nullptr);
}
// =============================================================================
// ObjectTable Handle Reference Counting Tests
// =============================================================================
TEST_CASE("ObjectTable RetainHandle increments ref count", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
X_STATUS status = table.RetainHandle(handle);
CHECK(status == X_STATUS_SUCCESS);
// Need two releases to remove
table.ReleaseHandle(handle);
auto still_there = table.LookupObject<XObject>(handle);
CHECK(still_there.get() == obj);
table.ReleaseHandle(handle); // This should remove it
table.Reset();
}
TEST_CASE("ObjectTable ReleaseHandle removes at zero refs", "[kernel][object_table]") {
InitTestLogging();
DestructorCountReset reset;
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
X_STATUS status = table.ReleaseHandle(handle);
CHECK(status == X_STATUS_SUCCESS);
// Handle should be gone
auto found = table.LookupObject<XObject>(handle);
CHECK(found.get() == nullptr);
table.Reset();
}
TEST_CASE("ObjectTable ReleaseHandle on invalid handle returns error", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
X_STATUS status = table.ReleaseHandle(0xF8001234);
CHECK(status == X_STATUS_INVALID_HANDLE);
}
TEST_CASE("ObjectTable DuplicateHandle creates new handle for same object",
"[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE h1 = 0, h2 = 0;
table.AddHandle(obj, &h1);
table.DuplicateHandle(h1, &h2);
CHECK(h1 != h2);
// Both handles refer to same object
auto found1 = table.LookupObject<XObject>(h1);
auto found2 = table.LookupObject<XObject>(h2);
CHECK(found1.get() == obj);
CHECK(found2.get() == obj);
// Object should have both handles
CHECK(obj->handles().size() == 2);
table.Reset();
}
// =============================================================================
// ObjectTable Handle Release Tests
// =============================================================================
TEST_CASE("ObjectTable ReleaseHandle removes handle from table", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
// ReleaseHandle decrements ref count; at 0 it removes the handle
X_STATUS status = table.ReleaseHandle(handle);
CHECK(status == X_STATUS_SUCCESS);
auto found = table.LookupObject<XObject>(handle);
CHECK(found.get() == nullptr);
table.Reset();
}
TEST_CASE("ObjectTable ReleaseHandle updates object's handle list", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
obj->Retain(); // Keep alive after table releases
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
CHECK(obj->handles().size() == 1);
table.ReleaseHandle(handle);
CHECK(obj->handles().empty());
obj->Release();
table.Reset();
}
// =============================================================================
// ObjectTable Name Mapping Tests
// =============================================================================
TEST_CASE("ObjectTable AddNameMapping registers name", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
X_STATUS status = table.AddNameMapping("TestName", handle);
CHECK(status == X_STATUS_SUCCESS);
table.Reset();
}
TEST_CASE("ObjectTable duplicate name returns collision", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj1 = new TestObject();
auto* obj2 = new TestObject();
X_HANDLE h1 = 0, h2 = 0;
table.AddHandle(obj1, &h1);
table.AddHandle(obj2, &h2);
CHECK(table.AddNameMapping("SharedName", h1) == X_STATUS_SUCCESS);
CHECK(table.AddNameMapping("SharedName", h2) == X_STATUS_OBJECT_NAME_COLLISION);
table.Reset();
}
TEST_CASE("ObjectTable name collision is case-insensitive", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj1 = new TestObject();
auto* obj2 = new TestObject();
X_HANDLE h1 = 0, h2 = 0;
table.AddHandle(obj1, &h1);
table.AddHandle(obj2, &h2);
CHECK(table.AddNameMapping("TestName", h1) == X_STATUS_SUCCESS);
// Different case should still collide
CHECK(table.AddNameMapping("testname", h2) == X_STATUS_OBJECT_NAME_COLLISION);
CHECK(table.AddNameMapping("TESTNAME", h2) == X_STATUS_OBJECT_NAME_COLLISION);
table.Reset();
}
TEST_CASE("ObjectTable GetObjectByName returns not found for missing name",
"[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
X_HANDLE found = 0;
X_STATUS status = table.GetObjectByName("DoesNotExist", &found);
CHECK(status == X_STATUS_OBJECT_NAME_NOT_FOUND);
CHECK(found == X_INVALID_HANDLE_VALUE);
}
TEST_CASE("ObjectTable RemoveNameMapping clears name", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj = new TestObject();
X_HANDLE handle = 0;
table.AddHandle(obj, &handle);
table.AddNameMapping("TempName", handle);
table.RemoveNameMapping("TempName");
X_HANDLE found = 0;
CHECK(table.GetObjectByName("TempName", &found) == X_STATUS_OBJECT_NAME_NOT_FOUND);
table.Reset();
}
// =============================================================================
// ObjectTable Reset and Bulk Operations
// =============================================================================
TEST_CASE("ObjectTable Reset releases all objects", "[kernel][object_table]") {
InitTestLogging();
DestructorCountReset reset;
ObjectTable table;
auto* obj1 = new TestObject();
auto* obj2 = new TestObject();
auto* obj3 = new TestObject();
table.AddHandle(obj1, nullptr);
table.AddHandle(obj2, nullptr);
table.AddHandle(obj3, nullptr);
// Objects have ref count 2: 1 from new, 1 from AddHandle's Retain
// Release our initial ref so table owns them exclusively
obj1->Release();
obj2->Release();
obj3->Release();
CHECK(TestObject::destructor_count == 0);
table.Reset();
// Now Reset's Release brings ref count from 1 to 0, destroying them
CHECK(TestObject::destructor_count == 3);
}
TEST_CASE("ObjectTable GetAllObjects returns all objects", "[kernel][object_table]") {
InitTestLogging();
ObjectTable table;
auto* obj1 = new TestObject();
auto* obj2 = new TestObject();
table.AddHandle(obj1, nullptr);
table.AddHandle(obj2, nullptr);
auto all = table.GetAllObjects();
CHECK(all.size() == 2);
table.Reset();
}
// =============================================================================
// TODO: Tests requiring kernel integration
// =============================================================================
// TODO: Test handle 0xFFFFFFFF (CurrentProcess) - requires full KernelState
// TODO: Test handle 0xFFFFFFFE (CurrentThread) - requires XThread integration
// TODO: Test GetObjectsByType<T>() with real typed objects
// TODO: Test GetObjectByName with existing object - requires kernel_state for RetainHandle
// TODO: Test case-insensitive name lookup via GetObjectByName