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.sh that bootstraps + pins the ut-core version, then builds.
  • A Makefile that forwards just the needed variables to ut-core.
  • (Optional) A tiny run_tests.sh helper to execute the produced test binary.

Goal: Make tests easy to add and upgrade (bug-fixes), but safe from breaking major changes.


1) Directory Layout (Recommended)

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)

2) Version Pinning Strategy (Why & How)

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.


3) build.sh — Bootstrap, Pin, Build

This script ensures ut-core exists under test/ut-core, checks out a specific ref (tag/branch/commit), then calls your test Makefile.

Usage Examples

./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

Script Content

#!/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 $@

4) Makefile — Pass Only What ut-core Needs

This Makefile defines where your sources, headers, and build outputs live, and passes them to ut-core.

Makefile Content

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 Meanings

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.

5) (Optional) run_tests.sh

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}" "$@"

6) Writing Tests

Example C Test

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();
}

Example C++ Test (GTest-style)

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;
}

7) Quick Start

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
⚠️ **GitHub.com Fallback** ⚠️