Skip to main content

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:

  1. CHI_SERVER_CONF environment variable
  2. WRP_RUNTIME_CONF environment variable
  3. ~/.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

  1. Initialize once: Use a static g_initialized flag in a fixture to avoid redundant CHIMAERA_INIT calls. The init function has an internal static guard — calling it twice returns true immediately, but the fixture pattern keeps it explicit.

  2. Use task.Wait(): Always call Wait() on async futures before accessing results. There is no need for manual polling loops.

  3. Check return_code_: After Wait(), check task->return_code_ == 0 (or task->GetReturnCode() == 0) to verify success.

  4. Sleep after init: Add std::this_thread::sleep_for(500ms) after CHIMAERA_INIT to let worker threads start before submitting tasks.

  5. 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_*
  6. 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