How to use UT‐Core to write a simple app in C or CPP using the ut‐core subsystem - rdkcentral/ut-core GitHub Wiki
This guide provides a clean, reproducible way to add unit tests to a C or C++ project using ut-core, without exposing or copying the ut-core Makefile.
You'll get:
- A standard test directory layout you can drop into any repo.
- A
build.shthat bootstraps + pins theut-coreversion, then builds. - A
Makefilethat forwards just the needed variables tout-core. - (Optional) A tiny
run_tests.shhelper to execute the produced test binary.
Goal: Make tests easy to add and upgrade (bug-fixes), but safe from breaking major changes.
This layout assumes your upper-level directory (one level above test/) contains the build/ directory, where your libraries and final test binaries will reside.
project-root/
├─ src/ # Your production sources (.c/.cpp)
├─ include/ # Your headers (.h/.hpp)
├── build # Your project build output (libs, generated)
│ ├── bin
│ └── lib
└── test # Test harness lives here
├── build.sh # Bootstraps/pins ut-core + builds tests
├── Makefile # Minimal Makefile that calls ut-core Makefile
├── run_tests.sh # (Optional) Runs the built test binary
└── src # Test sources
├── cpp_source
│ ├── test_example.cpp # CPP Code
├── c_source
│ ├── test_example.c # C Code
└─ include/ # Test-only headers (optional)You want repeatable builds and safe upgrades. Two good options for pinning:
A. Pin to a maintained minor branch/tag (e.g., 4.x):
- Easy to move forward for bug-fixes.
- Low risk of breaking changes.
B. Pin to an exact commit SHA:
- Maximum reproducibility for CI.
- Manually advance to adopt fixes.
We'll implement both in build.sh so you can choose by setting the UT_CORE_REF variable.
This script ensures ut-core exists under test/ut-core, checks out a specific ref (tag/branch/commit), then calls your test Makefile.
./build.sh # Linux, C tests by default
CC=arm-linux-gnueabihf-gcc
./build.sh TARGET=arm VARIANT=C # ARM, C tests
CXX=arm-linux-gnueabihf-g++
./build.sh TARGET=arm VARIANT=CPP # ARM, C++ tests#!/usr/bin/env bash
set -euo pipefail
# --- Configuration ---
UT_CORE_DIR="$(dirname "$(realpath "$0")")/ut-core"
UT_CORE_REMOTE="https://github.com/rdkcentral/ut-core.git"
UT_CORE_REF="${UT_CORE_REF:-5.}"
TARGET="${TARGET:-linux}"
VARIANT="${VARIANT:-C}"
if [[ ! -d "$UT_CORE_DIR/.git" ]]; then
echo "[build.sh] Cloning ut-core into $UT_CORE_DIR"
git clone "$UT_CORE_REMOTE" "$UT_CORE_DIR"
else
echo "[build.sh] ut-core already present at $UT_CORE_DIR"
fi
# --- Resolve ut-core version ---
echo "[build.sh] Resolving ut-core ref: ${UT_CORE_REF}"
pushd "$UT_CORE_DIR" >/dev/null
git fetch --tags --prune --quiet
resolve_ref() {
local ref="$1"
# Why: "4." is not a git ref → treat as prefix and find matching tags
if [[ "$ref" =~ \.$ ]]; then
local prefix="${ref}"
local match
match="$(git tag --list "${prefix}*" | sort -V | tail -n 1 || true)"
if [[ -z "$match" ]]; then
echo "[build.sh] ERROR: No tag matches prefix '$prefix'"
exit 1
fi
echo "$match"
else
echo "$ref"
fi
}
FINAL_REF="$(resolve_ref "$UT_CORE_REF")"
echo "[build.sh] Using resolved ut-core ref: $FINAL_REF"
git checkout -qf "$FINAL_REF"
popd >/dev/null
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
export TARGET VARIANT
make $@
This Makefile defines where your sources, headers, and build outputs live, and passes them to ut-core.
ROOT_DIR := $(abspath $(CURDIR)/..)
TEST_DIR := $(abspath $(CURDIR))
UT_CORE_DIR ?= $(TEST_DIR)/ut-core
# Output locations (assumed under the parent project’s build/)
BIN_DIR ?= $(ROOT_DIR)/build/bin
LIB_DIR ?= $(ROOT_DIR)/build/lib
INC_DIRS += $(ROOT_DIR)/build/include $(TEST_DIR)/include
INC_DIRS += $(UT_CORE_DIR)/include
# The test binary name
TARGET_EXEC ?= mytest
# Optional linking example: link against your project libraries
# Use this pattern to add libraries you want to test against.
# For example, link your core libraries with runtime path set to build/lib.
# Modify or extend as needed.
YLDFLAGS := -Wl,-rpath,$(LIB_DIR) -L$(LIB_DIR)
# Source directories for your test code
ifneq ($(VARIANT),CPP) # CUNIT case
VARIANT = C
SRC_DIRS += $(TEST_DIR)/src/c_source
XCFLAGS += -DUT_CUNIT
else
VARIANT = CPP
SRC_DIRS += $(TEST_DIR)/src/cpp_source
CXXFLAGS += -std=c++17
CXXFLAGS += -Wall -Wformat -Werror=format
export CXXFLAGS
endif
export SRC_DIRS
export INC_DIRS
export TARGET_EXEC
export YLDFLAGS
export BIN_DIR
export VARIANT
.PHONY: all clean run list cleanall
all:
@echo "[Makefile] Building tests with ut-core (VARIANT=$(VARIANT))"
make -C ./ut-core VARIANT=$(VARIANT)
run: all
$(BIN_DIR)/$(TARGET_EXEC)
clean:
@echo "[Makefile] Cleaning local artifacts"
@rm -rf $(BIN_DIR)/*
make -C ./ut-core clean
cleanall:
make clean
make -C ./ut-core cleanall
@rm -rf ./ut-core
list:
make -C ./ut-core list| Variable | Description |
|---|---|
| SRC_DIRS | Directory containing your test sources (e.g., test/src). |
| INC_DIRS | Include paths for your project (build/include) and your test-only headers. |
| TARGET_EXEC | The name of your produced test binary (e.g., mytest). |
| YLDFLAGS | Example pattern for linking in your libraries (e.g., -lxfw -lxfw_platform) and defining runtime path. |
| BIN_DIR | Where your test binary will be placed (assumed under your upper-level build/bin). |
| VARIANT | Set to C or CPP to switch between C and C++ builds. |
This script runs your built test binary on macOS or Linux. It sets the appropriate runtime library load path so that shared libraries in your build/lib directory can be found at runtime.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd -P)"
BIN="${ROOT_DIR}/build/bin/mytest"
LIBDIR="${ROOT_DIR}/build/bin"
if [[ ! -x "${BIN}" ]]; then
echo "Error: test binary not found: ${BIN}"
echo "Hint: run './build.sh' in the test directory first."
exit 1
fi
OS="$(uname -s || echo unknown)"
case "${OS}" in
Darwin) export DYLD_LIBRARY_PATH="${LIBDIR}:${DYLD_LIBRARY_PATH:-}" ;;
Linux) export LD_LIBRARY_PATH="${LIBDIR}:${LD_LIBRARY_PATH:-}" ;;
*) export LD_LIBRARY_PATH="${LIBDIR}:${LD_LIBRARY_PATH:-}" ;;
esac
echo "[run_tests.sh] Running ${BIN} with library path set for ${OS}"
exec "${BIN}" "$@"Uses the built-in ut.h from ut-core.
#include <ut.h>
#include <ut_kvp_profile.h>
#define KVP_VALID_TEST_ASSERT_YAML_FILE "file_dont_exist"
static UT_test_suite_t *gpAssertSuite1 = NULL;
int group_test_init_function(void)
{
/* I'm called before the group is ran to init rest requirements */
}
int group_test_cleanup_function(void)
{
/* I'm called post test to clean up the requirements */
}
void test_ut_kvp_get_instance( void )
{
ut_kvp_instance_t *pInstance;
UT_LOG_STEP( "test_ut_kvp_get_instance - start" );
pInstance = ut_kvp_profile_getInstance();
UT_ASSERT( pInstance != NULL );
UT_LOG_STEP( "test_ut_kvp_get_instance - end" );
}
void test_ut_kvp_profile_open( void )
{
ut_kvp_status_t status;
UT_LOG_STEP( "test_ut_kvp_profile_open - start" );
status = ut_kvp_profile_open( KVP_VALID_TEST_ASSERT_YAML_FILE );
UT_ASSERT( status != UT_KVP_STATUS_SUCCESS );
UT_LOG_STEP( "test_ut_kvp_profile_open - end" );
}
void register_kvp_profile_testing_functions(void)
{
gpAssertSuite1 = UT_add_suite_withGroupID("assert open / close", &group_test_init_function, &group_test_cleanup_function, UT_TESTS_L1);
assert(gpAssertSuite1 != NULL);
UT_add_test(gpAssertSuite1, "profile open()", test_ut_kvp_profile_open);
UT_add_test(gpAssertSuite1, "profile getInstance()", test_ut_kvp_get_instance);
/* Add other suites / tests here */
}
int main(int argc, char** argv)
{
/* Register tests as required, then call the UT-main to support switches and triggering */
UT_init(argc, argv);
register_kvp_profile_testing_functions();
UT_run_tests();
}Uses C++ Test syntax, similar to gtest, but also adds menu subsystems.
#include <ut.h>
class UTGTestL1 : public UTCore
{
public:
UTGTestL1() : UTCore() {}
~UTGTestL1() override = default;
void SetUp() override
{
// Code to set up resources before each test
}
void TearDown() override
{
// Code to clean up resources after each test
}
};
// Automatically register test suite before test execution
//UT_ADD_TEST_TO_GROUP(UTGTestL1, UT_TESTS_L1) //Commenting out for the time being
UT_ADD_TEST(UTGTestL1, TestGtestL1Equal)
{
UT_ASSERT_EQUAL(1, 1); // Basic test case
}
UT_ADD_TEST(UTGTestL1, TestGtestL1NotEqual)
{
UT_ASSERT_NOT_EQUAL(1, 2);
}
UT_ADD_TEST(UTGTestL1, TestGtestL1GreaterThan)
{
UT_ASSERT_GREATER(2, 1);
}
UT_ADD_TEST(UTGTestL1, SameNameTest)
{
UT_PASS("This test has the same name as another test in a different suite");
}
int main(int argc, char **argv)
{
UT_status_t result;
UT_init(argc, argv);
result = UT_run_tests();
return result;
}To begin, navigate to your test directory and run the scripts:
cd test
./build.sh # Build tests by default in x86 for VARIANT=C(C is default)
./build.sh clean # Clean the binaries & objects generated.
./build.sh VARIANT=CPP # Build tests by default in x86 for VARIANT=CPP
./build.sh TARGET=arm # Build tests using the external compiler defined by CC= & CX=
./build.sh TARGET=arm clean # Clean the binary & objects generated
./run_tests.sh # Run test binary