Unit Test Skill Instructions
Unit testing involves writing test cases for individual components of the codebase to verify their correctness in isolation. This helps catch bugs early and ensures that each part of the system behaves as expected.
Writing Unit Tests
Unit Test Fixtures
- •When adding unit tests, reuse existing fixture classes defined in
fixtures.hppwhen possible. These provide common setup/teardown for subsystems like simulate, async runtime, etc. - •Use
get_machine_state()to check subsystem initialization state and avoid redundant setup. For example, if testing a feature that requires simulate, check if it's already initialized before callingsetup_simulate(). - •The
SetUp()method in the test fixture should follow the same order as insrc/main.c: configurations, then resource pools, LPC compiler, followed by world simulation and mudlib vital objects and epilogue. This ensures that all dependencies are properly initialized for the tests. - •The
TearDown()method should clean up resources in the reverse order of setup to ensure proper teardown without leaving dangling pointers or unfreed memory. Seedo_shutdown()insrc/simulate.cfor reference.
CMake settings for unit test programs
- •In
CMakeLists.txt, link unit-test program toGTest::gtest_mainand the specific libraries needed for the test (e.g.,async,socket,stemfor driver tests) - •For tests that require mudlib access, add a custom command to copy the testing mudlib in examples/ to the build directory. Pattern:
cmake
add_custom_command(TARGET test_lpc_compiler POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/examples/m3_mudlib ${CMAKE_CURRENT_BINARY_DIR}/m3_mudlib COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/examples/m3.conf ${CMAKE_CURRENT_BINARY_DIR} )
LPC Object Loading in Tests
Make sure init_master() is called in the fixture setup before loading LPC objects, as the master object is responsible for approving load_object() calls. Set current_object to master_ob when loading objects in tests to ensure the master applies to approve the load.
When testing LPC compilation/object loading, use load_object() with the second parameter pre_text to load LPC source code directly from a string. This avoids the need for filesystem access and allows for more self-contained unit tests. Pattern:
// Use a fixture that sets up simulate and master object
// simul_efun is optional depending on test needs
// Load from inline LPC code (no filesystem required)
current_object = master_ob; // Set current_object for master applies to approve the load_object() call
object_t* obj = load_object("test_obj.c", "void create() { }\n");
ASSERT_NE(obj, nullptr);
EXPECT_STREQ(obj->name, "test_obj");
// Cleanup
destruct_object(obj);
Calling LPC functions from tests
- •Get the shared string for the function name using
findstring(e.g.,findstring("create")) - •Call
find_function()to get the function index from the program and theprogram_tpointer if the function is inherited from a parent program. Note thatfind_functionrequires pointer to a shared string returned fromfindstring. Pattern:
// found_prog is the program where the function is defined.
// index is the COMPILER function index (position in found_prog's function_table).
// function index offset for inherited functions is returned via fio.
// variable index offset for inherited globals is returned via vio.
program_t* found_prog = find_function(prog, findstring("add"), &index, &fio, &vio);
ASSERT_NE(found_prog, nullptr);
- •If
found_progis different fromprog, the function is inherited. Theindexcan be used to obtain thecompiler_function_tinfound_prog's function_table where function address and argument count are stored. CRITICAL:indexis a compiler index, NOT a runtime index. Convert it usingfound_prog->function_table[index].runtime_indexthen addfiofor inheritance offset:(found_prog->function_table[index].runtime_index + fio)gives the runtime dispatch index inprog. - •Push arguments onto the stack using
push_number(),push_string(), etc. Find available push functions in src/stack.c. - •Call
call_function()with the actual function runtime index and number of arguments already pushed onto the stack (num_args). Pattern:
// Convert compiler index to runtime index, then add inheritance offset int runtime_index = found_prog->function_table[index].runtime_index + fio; // For void functions, ret can be a dummy svalue_t since it won't be used svalue_t ret; call_function (prog, runtime_index, num_args, &ret);
Socket Testing Patterns
Tests using network sockets require special initialization handling:
Header Includes:
#include <gtest/gtest.h>
extern "C" {
#include "async/async_runtime.h"
#include "port/socket_comm.h" // Provides create_test_socket_pair()
}
Windows Socket Initialization:
- •Make sure the fixture calls
WSAStartup()/WSACleanup()ifWINSOCKis defined. This can be done via a global test environment in GoogleTest. Pattern:
#ifdef WINSOCK
class WinsockEnvironment : public ::testing::Environment {
// SetUp() calls WSAStartup(), TearDown() calls WSACleanup()
};
static ::testing::Environment* const winsock_env =
::testing::AddGlobalTestEnvironment(new WinsockEnvironment);
#endif
Unit Test File Organization
- •The testing mudlib is copied to ${CMAKE_CURRENT_BINARY_DIR} so the unit-test programs of different configurations can be launched from this directory and locate the mudlib config file
m3.conffrom current working directory. Pattern:
cd out/build/linux/tests/test_lpc_compiler ./RelWithDebInfo/test_lpc_compiler
- •Individual test files (e.g.,
test_lpc_compiler.cpp): Include common header, write tests, - •Common header
fixtures.hpp: Define test fixtures for shared setup (e.g.,LPCCompilerTestthat initializes simulate subsystem). Use theTestsuffix for fixture classes to follow GoogleTest conventions.