ChiMod Unit Testing Guide
This guide covers how to create unit tests for Chimaera modules (ChiMods). The testing framework allows both the runtime and client to run in a single process, enabling integration testing without multi-process coordination.
Test Environment Setup
Environment Variables
Unit tests require specific environment variables for module discovery and configuration:
# Path to compiled ChiMod libraries (build/bin directory)
export CHI_REPO_PATH="/path/to/build/bin"
# Library path for dynamic loading
export LD_LIBRARY_PATH="/path/to/build/bin:$LD_LIBRARY_PATH"
# Optional: Specify a custom configuration file
export CHI_SERVER_CONF="/path/to/chimaera_default.yaml"
Module Discovery: The runtime scans both CHI_REPO_PATH and LD_LIBRARY_PATH for ChiMod shared libraries (e.g., libchimaera_bdev.so). Point these at your build/bin directory.
Configuration Files
Tests use the same configuration format as production. The runtime looks for configuration in this order:
CHI_SERVER_CONFenvironment variableWRP_RUNTIME_CONFenvironment variable~/.chimaera/chimaera.yaml
A minimal test configuration:
networking:
port: 9413
runtime:
num_threads: 4
queue_depth: 1024
compose:
- mod_name: chimaera_bdev
pool_name: "ram::chi_default_bdev"
pool_query: local
pool_id: "301.0"
bdev_type: ram
capacity: "512MB"
See the Configuration Reference for all parameters.
Test Framework
The project uses a custom lightweight test framework defined in context-runtime/test/simple_test.h. It provides macros similar to Catch2:
#include "simple_test.h"
TEST_CASE("Descriptive test name", "[tag1][tag2]") {
SECTION("subsection name") {
REQUIRE(condition);
REQUIRE_FALSE(condition);
REQUIRE_NOTHROW(expression);
FAIL("explicit failure message");
INFO("diagnostic message: " << value);
}
}
Test Runners
There are two ways to define main():
Option 1 — SIMPLE_TEST_MAIN() macro (preferred for tests that don't need the runtime):
SIMPLE_TEST_MAIN()
Option 2 — Custom main() with runtime initialization (required for tests that submit tasks):
int main(int argc, char **argv) {
// Initialize Chimaera runtime + client in one process
if (!chi::CHIMAERA_INIT(chi::ChimaeraMode::kClient, true)) {
HLOG(kError, "Failed to initialize Chimaera runtime");
return 1;
}
// Run tests with optional filter from command line
std::string filter = "";
if (argc > 1) {
filter = argv[1];
}
return SimpleTest::run_all_tests(filter);
}
chi::CHIMAERA_INIT(chi::ChimaeraMode::kClient, true) starts both the runtime (worker threads, task queues) and the client library in a single process. The true flag means "also start the embedded runtime."
Test Fixture Pattern
Most tests use a fixture class that initializes the runtime once across all test cases:
#include "simple_test.h"
#include <chimaera/chimaera.h>
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
namespace {
bool g_initialized = false;
}
class MyModuleFixture {
public:
MyModuleFixture() {
if (!g_initialized) {
bool success = chi::CHIMAERA_INIT(chi::ChimaeraMode::kClient, true);
if (success) {
g_initialized = true;
std::this_thread::sleep_for(500ms); // Allow workers to start
}
}
}
};
Instantiate the fixture at the top of each TEST_CASE:
TEST_CASE("My test", "[mymod]") {
MyModuleFixture fixture;
REQUIRE(g_initialized);
// ... test body ...
}
Complete Test Example
This example demonstrates creating a pool, submitting async tasks, and checking results — following the patterns used in the actual codebase (e.g., test_compose.cc, test_streaming.cc, test_bdev_chimod.cc).
#include "simple_test.h"
#include <chimaera/chimaera.h>
#include <chimaera/admin/admin_client.h>
#include <chimaera/bdev/bdev_client.h>
#include <chimaera/config_manager.h>
#include <fstream>
using namespace std::chrono_literals;
namespace {
bool g_initialized = false;
}
class ComposeFixture {
public:
ComposeFixture() {
if (!g_initialized) {
bool success = chi::CHIMAERA_INIT(chi::ChimaeraMode::kClient, true);
if (success) {
g_initialized = true;
std::this_thread::sleep_for(500ms);
}
}
}
};
/**
* Helper: write a compose config to a temp file
*/
std::string CreateComposeConfig() {
std::string path = "/tmp/test_compose_config.yaml";
std::ofstream f(path);
f << "runtime:\n"
<< " num_threads: 4\n"
<< "\n"
<< "networking:\n"
<< " port: 9413\n"
<< "\n"
<< "compose:\n"
<< "- mod_name: chimaera_bdev\n"
<< " pool_name: /tmp/test_bdev.dat\n"
<< " pool_query: dynamic\n"
<< " pool_id: 200.0\n"
<< " capacity: 10MB\n"
<< " bdev_type: file\n";
f.close();
return path;
}
TEST_CASE("Parse compose configuration", "[compose]") {
ComposeFixture fixture;
REQUIRE(g_initialized);
std::string config_path = CreateComposeConfig();
auto* config_manager = CHI_CONFIG_MANAGER;
REQUIRE(config_manager != nullptr);
REQUIRE(config_manager->LoadYaml(config_path));
const auto& compose_config = config_manager->GetComposeConfig();
REQUIRE(compose_config.pools_.size() >= 1);
// Find our test pool
bool found = false;
for (const auto& pool : compose_config.pools_) {
if (pool.mod_name_ == "chimaera_bdev" &&
pool.pool_name_ == "/tmp/test_bdev.dat") {
REQUIRE(pool.pool_id_.major_ == 200);
REQUIRE(pool.pool_id_.minor_ == 0);
REQUIRE(pool.pool_query_.IsDynamicMode());
found = true;
break;
}
}
REQUIRE(found);
}
TEST_CASE("Admin client Compose", "[compose]") {
ComposeFixture fixture;
REQUIRE(g_initialized);
std::string config_path = CreateComposeConfig();
auto* config_manager = CHI_CONFIG_MANAGER;
REQUIRE(config_manager->LoadYaml(config_path));
auto* admin_client = CHI_ADMIN;
REQUIRE(admin_client != nullptr);
const auto& compose_config = config_manager->GetComposeConfig();
// Submit compose tasks asynchronously
for (const auto& pool_config : compose_config.pools_) {
auto task = admin_client->AsyncCompose(pool_config);
task.Wait();
REQUIRE(task->GetReturnCode() == 0);
}
// Verify pool exists by using it
chi::PoolId bdev_pool_id(200, 0);
chimaera::bdev::Client bdev_client(bdev_pool_id);
auto alloc_task = bdev_client.AsyncAllocateBlocks(
chi::PoolQuery::Local(), 1024);
alloc_task.Wait();
REQUIRE(alloc_task->GetReturnCode() == 0);
}
int main(int argc, char **argv) {
if (!chi::CHIMAERA_INIT(chi::ChimaeraMode::kClient, true)) {
HLOG(kError, "Failed to initialize Chimaera runtime");
return 1;
}
std::string filter = "";
if (argc > 1) {
filter = argv[1];
}
return SimpleTest::run_all_tests(filter);
}
Async Task Patterns
Basic async submit and wait
auto task = client.AsyncCreate(pool_query, pool_name, pool_id);
task.Wait();
REQUIRE(task->return_code_ == 0);
task is a chi::Future<T>. After Wait(), access result fields via task->field_name_.
Creating a module pool then using it
chimaera::MOD_NAME::Client client(pool_id);
// Create the container
chi::PoolQuery pool_query = chi::PoolQuery::Dynamic();
auto create_task = client.AsyncCreate(pool_query, "my_pool", pool_id);
create_task.Wait();
client.pool_id_ = create_task->new_pool_id_;
REQUIRE(create_task->return_code_ == 0);
// Use the module
auto task = client.AsyncCustom(pool_query, input_data, 42);
task.Wait();
REQUIRE(task->return_code_ == 0);
Multiple parallel tasks
std::vector<chi::Future<SomeTask>> tasks;
for (int i = 0; i < num_tasks; ++i) {
tasks.push_back(client.AsyncOperation(pool_query, params));
}
for (auto& task : tasks) {
task.Wait();
REQUIRE(task->return_code_ == 0);
}
CMake Integration
Add unit tests to your module's CMakeLists.txt. Follow the pattern used in context-runtime/test/unit/CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
# Test executable
set(TEST_TARGET my_module_tests)
add_executable(${TEST_TARGET} test_my_module.cc)
target_include_directories(${TEST_TARGET} PRIVATE
${CHIMAERA_ROOT}/include
${CHIMAERA_ROOT}/test # For simple_test.h
${CHIMAERA_ROOT}/modules/admin/include
${CHIMAERA_ROOT}/modules/bdev/include
)
target_link_libraries(${TEST_TARGET}
chimaera_cxx # Main Chimaera library
chimaera_admin_client # Admin module client
chimaera_bdev_client # Bdev module client
hshm::cxx # HermesShm library
${CMAKE_THREAD_LIBS_INIT} # Threading support
)
set_target_properties(${TEST_TARGET} PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
# Install test executable
install(TARGETS ${TEST_TARGET} RUNTIME DESTINATION bin)
# CTest registration
if(WRP_CORE_ENABLE_TESTS)
add_test(
NAME my_module_all_tests
COMMAND ${TEST_TARGET}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
set_tests_properties(my_module_all_tests PROPERTIES
ENVIRONMENT "CHI_REPO_PATH=${CMAKE_BINARY_DIR}/bin;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/bin:$ENV{LD_LIBRARY_PATH}"
TIMEOUT 180
)
endif()
If your module has its own runtime and client libraries, link those as well:
target_link_libraries(${TEST_TARGET}
my_module_runtime
my_module_client
chimaera_cxx
hshm::cxx
${CMAKE_THREAD_LIBS_INIT}
)
Building and Running Tests
# Build
cd /workspace/build
cmake ..
make -j$(nproc)
sudo make install # Required — tests use rpath-based linking
# Run all tests in a binary
./bin/my_module_tests
# Run tests matching a tag filter
./bin/my_module_tests "[compose]"
# Run tests matching a name substring
./bin/my_module_tests "Parse compose"
# Run via CTest
ctest -R my_module
Best Practices
-
Initialize once: Use a static
g_initializedflag in a fixture to avoid redundantCHIMAERA_INITcalls. The init function has an internal static guard — calling it twice returnstrueimmediately, but the fixture pattern keeps it explicit. -
Use
task.Wait(): Always callWait()on async futures before accessing results. There is no need for manual polling loops. -
Check
return_code_: AfterWait(), checktask->return_code_ == 0(ortask->GetReturnCode() == 0) to verify success. -
Sleep after init: Add
std::this_thread::sleep_for(500ms)afterCHIMAERA_INITto let worker threads start before submitting tasks. -
Clean up shared memory between runs: If a previous test crashed, stale shared memory segments can block the next run:
rm -f /dev/shm/chimaera_* -
Kill stale processes on port conflicts: Tests bind to port 9413. If a previous run left a zombie:
sudo kill -9 $(sudo lsof -t -i :9413) 2>/dev/null