Add initial version of firmware-test-builder

Introduce the following features to firmware-test-builder:

* Testing on the host machine (PC)
* Provides CMake functions for building/running/evaluating tests
  * Checking prerequisites (git, c-picker, libclang, etc.)
  * Fetching and building CppUTest
  * Defining and building unit test suites (i.e. separate binaries)
  * Handling c-picker based extraction of code snippets
  * Registering test suites to CTest (CMake's test system) which runs
    all the test binaries
* Generating coverage report
* Documentation of the system

Signed-off-by: Imre Kis <imre.kis@arm.com>
Change-Id: Ic0a1af55bef07c6e76071193caa94a9a48f9041f
diff --git a/cmake/Coverage.cmake b/cmake/Coverage.cmake
new file mode 100644
index 0000000..b7a6439
--- /dev/null
+++ b/cmake/Coverage.cmake
@@ -0,0 +1,286 @@
+#
+# Copyright (c) 2020-2021, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+#[===[.rst:
+Coverage CMake module
+---------------------
+
+Control flow
+^^^^^^^^^^^^
+
+Using the code coverage feature of the system starts with including
+``Coverage`` module. This will implicitly check if all the requirements for
+generating coverage are fulfilled. This includes checking the following
+conditions.
+
+- Compiler is GCC
+- lcov executables exist
+- ``c-picker-coverage-mapper`` is available
+
+As the next step it sets the compiler flags to make GCC to generate binaries
+with coverage information.
+
+
+Variables
+^^^^^^^^^
+
+The module sets the following variables while it's checking its prerequisites.
+
+.. cmake:variable:: LCOV_COMMAND
+
+Path of lcov executable
+
+.. cmake:variable:: GENHTML_COMMAND
+
+Path of genhtml executable which is part of the lcov package.
+
+.. cmake:variable:: CPICKER_COVERAGE_MAPPER_COMMAND
+
+Path of ``c-picker-coverage-mapper`` executable which is provided by c-picker
+pip package.
+
+
+Functions
+^^^^^^^^^
+
+The module also contains functions for setting up the coverage feature.
+
+#]===]
+
+include_guard(DIRECTORY)
+
+# Checking GCC
+if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+	message(FATAL_ERROR "Coverage measurement is only supported when using GCC")
+endif()
+
+# Checking lcov
+find_program(LCOV_COMMAND "lcov")
+if (NOT LCOV_COMMAND)
+	message(FATAL_ERROR "Please install lcov")
+endif()
+
+# Checking c-picker-coverage-mapper
+find_program(CPICKER_COVERAGE_MAPPER_COMMAND "c-picker-coverage-mapper")
+if (NOT CPICKER_COVERAGE_MAPPER_COMMAND)
+	message(FATAL_ERROR "Please install c-picker-coverage-mapper using pip (part of c-picker)")
+endif()
+
+# Checking genhtml
+find_program(GENHTML_COMMAND "genhtml")
+if (NOT GENHTML_COMMAND)
+	message(FATAL_ERROR "Please install genhtml with genhtml (part of lcov)")
+endif()
+
+# Including this file enables code coverage measurement by adding the necessary compiler and
+# linker flags.
+set(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage")
+set(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage -fno-exceptions")
+set(CMAKE_EXE_LINKER_FLAGS "-fprofile-arcs -ftest-coverage")
+
+# Adding custom targets
+add_custom_target(coverage)
+add_custom_target(coverage_report)
+
+# Adds a file to the dependency list of the target by inserting an accessory
+# custom target. The name of the custom target is properly escaped.
+function(add_coverage_dependency)
+	set(_OPTIONS_ARGS)
+	set(_ONE_VALUE_ARGS TARGET DEPENDS)
+	set(_MULTI_VALUE_ARGS)
+	cmake_parse_arguments(_MY_PARAMS "${_OPTIONS_ARGS}" "${_ONE_VALUE_ARGS}" "${_MULTI_VALUE_ARGS}" ${ARGN})
+
+	set(TARGET ${_MY_PARAMS_TARGET})
+	set(DEPENDS ${_MY_PARAMS_DEPENDS})
+
+	string(REGEX REPLACE "\\/" "_" CUSTOM_TARGET_SUFFIX ${DEPENDS})
+
+	add_custom_target(${TARGET}_target_${CUSTOM_TARGET_SUFFIX} DEPENDS ${DEPENDS})
+	add_dependencies(${TARGET} ${TARGET}_target_${CUSTOM_TARGET_SUFFIX})
+endfunction()
+
+#[===[.rst:
+.. cmake:command:: coverage_generate
+
+  .. code-block:: cmake
+
+    coverage_generate(
+    	NAME test_name
+    	SOURCE_DIR source_directory
+    	BINARY_DIR binary_directory
+    	OUTPUT_FILE output_file
+    )
+
+  The function outputs an lcov info file for further processing. It also handles
+  the remapping of the coverage of the c-picker generated files.
+
+  Control flow:
+
+  1. Running the ``lcov`` command for collecting the coverage data from the
+     available ``.gcda`` and ``.gcno`` files in the ``BINARY_DIR``.
+
+  2. The output of previous step is processed by ``c-picker-coverage-mapper``.
+     This will remap the coverage of files in ``CPICKER_CACHE_PATH`` to the
+     original source files.
+
+  3. Adds the output file to the ``coverage`` target's dependency list.
+
+  Inputs:
+
+  ``NAME``
+    Test name included in lcov info file
+
+  ``SOURCE_DIR``
+    Directory of source files
+
+  ``BINARY_DIR``
+    Directory of the ``.gcda`` and ``.gcno`` files
+
+  ``OUTPUT_FILE``
+    Output lcov coverage info file
+
+  Global dependencies:
+
+  ``CPICKER_CACHE_PATH``
+    Root directory of the c-picker generated files
+
+
+#]===]
+function(coverage_generate)
+	set(_OPTIONS_ARGS)
+	set(_ONE_VALUE_ARGS NAME SOURCE_DIR BINARY_DIR OUTPUT_FILE)
+	set(_MULTI_VALUE_ARGS)
+	cmake_parse_arguments(_MY_PARAMS "${_OPTIONS_ARGS}" "${_ONE_VALUE_ARGS}" "${_MULTI_VALUE_ARGS}" ${ARGN})
+
+	set(TEST_NAME ${_MY_PARAMS_NAME})
+	set(SOURCE_DIR ${_MY_PARAMS_SOURCE_DIR})
+	set(BINARY_DIR ${_MY_PARAMS_BINARY_DIR})
+	set(TEMP_FILE ${_MY_PARAMS_OUTPUT_FILE}_temp)
+	set(OUTPUT_FILE ${_MY_PARAMS_OUTPUT_FILE})
+
+	# Collecting information from .gcda and .gcno files into an lcov .info file
+	# Mapping c-picker generated files' coverage info to the original source lines
+	add_custom_command(
+		OUTPUT ${TEMP_FILE} ${OUTPUT_FILE}
+		COMMAND ${LCOV_COMMAND}
+			--capture
+			--test-name ${TEST_NAME}
+			--directory ${BINARY_DIR}
+			--base-directory ${SOURCE_DIR}
+			--output-file ${TEMP_FILE}
+		COMMAND ${CPICKER_COVERAGE_MAPPER_COMMAND}
+			--input ${TEMP_FILE}
+			--output ${OUTPUT_FILE}
+			--mapping-path ${CPICKER_CACHE_PATH}
+	)
+
+	add_coverage_dependency(
+		TARGET coverage
+		DEPENDS ${OUTPUT_FILE}
+	)
+endfunction()
+
+#[===[.rst:
+.. cmake:command:: coverage_filter
+
+  .. code-block:: cmake
+
+    coverage_filter(
+    	INPUT_FILE input_file
+    	OUTPUT_FILE output_file
+    	INCLUDE_DIRECTORY include_directory
+    )
+
+  The function filters the coverage data by including only the coverage of the
+  files of ``INCLUDE_DIRECTORY`` or its subdirectories. It adds the filtered
+  output file to the ``coverage`` target's dependency list.
+
+  Inputs:
+
+  ``INPUT_FILE``
+    Input lcov coverage info file
+
+  ``OUTPUT_FILE``
+    Output lcov coverage info file
+
+  ``INCLUDE_DIRECTORY``
+    Root directory of included files
+
+#]===]
+function(coverage_filter)
+	set(_OPTIONS_ARGS)
+	set(_ONE_VALUE_ARGS INPUT_FILE OUTPUT_FILE INCLUDE_DIRECTORY)
+	set(_MULTI_VALUE_ARGS)
+	cmake_parse_arguments(_MY_PARAMS "${_OPTIONS_ARGS}" "${_ONE_VALUE_ARGS}" "${_MULTI_VALUE_ARGS}" ${ARGN})
+
+	set(INPUT_FILE ${_MY_PARAMS_INPUT_FILE})
+	set(OUTPUT_FILE ${_MY_PARAMS_OUTPUT_FILE})
+	set(INCLUDE_DIRECTORY ${_MY_PARAMS_INCLUDE_DIRECTORY})
+
+	# The pattern must be an absolute path ending with an asterisk
+	get_filename_component(INCLUDE_DIRECTORY_ABSPATH "${INCLUDE_DIRECTORY}" ABSOLUTE)
+	set(INCLUDE_DIRECTORY_ABSPATH "${INCLUDE_DIRECTORY_ABSPATH}/*")
+
+	add_custom_command(
+		OUTPUT ${OUTPUT_FILE}
+		COMMAND ${LCOV_COMMAND}
+			--extract ${INPUT_FILE} \"${INCLUDE_DIRECTORY_ABSPATH}\"
+			--output-file ${OUTPUT_FILE}
+		DEPENDS ${INPUT_FILE}
+	)
+
+	add_coverage_dependency(
+		TARGET coverage
+		DEPENDS ${OUTPUT_FILE}
+	)
+endfunction()
+
+#[===[.rst:
+.. cmake:command:: coverage_generate_report
+
+  .. code-block:: cmake
+
+    coverage_generate_report(
+    	INPUT_FILE input_file
+    	OUTPUT_DIRECTORY output_directory
+    )
+
+  The function generates a HTML coverage report from the lcov info file into
+  the ``OUTPUT_DIRECTORY``. It adds the output directory to the
+  ``coverage_report`` target's dependency list.
+
+  Inputs:
+
+  ``INPUT_FILE``
+    Input lcov coverage info file
+
+  ``OUTPUT_DIRECTORY``
+    Output directory of the coverage report where the ``index.html`` is the
+    root file of the report.
+
+#]===]
+function(coverage_generate_report)
+	set(_OPTIONS_ARGS)
+	set(_ONE_VALUE_ARGS INPUT_FILE OUTPUT_DIRECTORY)
+	set(_MULTI_VALUE_ARGS)
+	cmake_parse_arguments(_MY_PARAMS "${_OPTIONS_ARGS}" "${_ONE_VALUE_ARGS}" "${_MULTI_VALUE_ARGS}" ${ARGN})
+
+	set(INPUT_FILE ${_MY_PARAMS_INPUT_FILE})
+	set(OUTPUT_DIRECTORY ${_MY_PARAMS_OUTPUT_DIRECTORY})
+
+	add_custom_command(
+		OUTPUT ${OUTPUT_DIRECTORY}
+		COMMAND genhtml ${INPUT_FILE}
+			--show-details
+			--output-directory ${OUTPUT_DIRECTORY}
+		DEPENDS ${INPUT_FILE}
+	)
+
+	add_coverage_dependency(
+		TARGET coverage_report
+		DEPENDS ${OUTPUT_DIRECTORY}
+	)
+endfunction()
diff --git a/cmake/FindLibClang.cmake b/cmake/FindLibClang.cmake
new file mode 100644
index 0000000..c371449
--- /dev/null
+++ b/cmake/FindLibClang.cmake
@@ -0,0 +1,90 @@
+#
+# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+#[=======================================================================[.rst:
+FindLibClang
+------------
+
+CMake module for finding the LibClang library.
+
+Control flow
+^^^^^^^^^^^^
+
+1. Running ``llvm-config`` if exists
+2. Searching for library at common places
+3. Searching in Windows registry if available
+
+
+Imported Targets
+^^^^^^^^^^^^^^^^
+
+This module provides the following imported targets, if found:
+
+``LibClang``
+  The Clang library
+
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+This will define the following variables:
+
+``LibClang_FOUND``
+  True if the system has the Clang library.
+``LibClang_LIBRARY_DIRS``
+  Libraries needed to link to Clang.
+
+#]=======================================================================]
+
+
+# 1. Use llvm-config
+find_program(_LLVM_CONFIG_COMMAND "llvm-config")
+
+if (_LLVM_CONFIG_COMMAND)
+	message(STATUS "Setting LibClang_LIBRARY_DIRS using ${_LLVM_CONFIG_COMMAND}")
+
+	execute_process(
+		COMMAND ${_LLVM_CONFIG_COMMAND} --libdir
+		OUTPUT_VARIABLE _LLVM_CONFIG_OUTPUT
+	)
+
+	# Stripping newline
+	string(STRIP ${_LLVM_CONFIG_OUTPUT} LibClang_LIBRARY_DIRS)
+endif()
+
+# 2. Try to find as library
+if (NOT LibClang_LIBRARY_DIRS)
+	message(STATUS "Setting LibClang_LIBRARY_DIRS based on common directories list")
+
+	set(LIBCLANG_COMMON_PATHS
+		/usr/lib/llvm-9/lib
+		/usr/lib/llvm-8/lib
+		/usr/lib/llvm-7/lib
+		/usr/lib/llvm-6.0/lib)
+
+	if (WIN32)
+		set(CMAKE_FIND_LIBRARY_PREFIXES "lib")
+		set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
+
+		get_filename_component(LLVM_PATH_FROM_REGISTRY [HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\LLVM\\LLVM] ABSOLUTE)
+		list(APPEND LIBCLANG_COMMON_PATHS "${LLVM_PATH_FROM_REGISTRY}/bin")
+	endif()
+
+	find_library(_LIBCLANG_PATH
+		NAMES clang
+		HINTS ${LIBCLANG_COMMON_PATHS}
+	)
+
+	if (_LIBCLANG_PATH)
+		get_filename_component(LibClang_LIBRARY_DIRS ${_LIBCLANG_PATH} DIRECTORY)
+	endif()
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(LibClang
+	"Please install llvm-config or set LibClang path manually"
+	LibClang_LIBRARY_DIRS
+)
diff --git a/cmake/UnitTest.cmake b/cmake/UnitTest.cmake
new file mode 100644
index 0000000..d61ed9a
--- /dev/null
+++ b/cmake/UnitTest.cmake
@@ -0,0 +1,322 @@
+#
+# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+#[===[.rst:
+UnitTest CMake module
+---------------------
+
+Control flow
+^^^^^^^^^^^^
+
+1. Setting :cmake:variable:`CLANG_LIBRARY_PATH`
+
+  1. Using :cmake:variable:`CLANG_LIBRARY_PATH` CMake variable
+
+  2. Using ``CLANG_LIBRARY_PATH`` environment variable
+
+  3. Trying to find by ``find_package`` function which calls :cmake:module:`FindLibClang`.
+
+2. Checking if ``c-picker`` command is available
+
+
+Variables
+^^^^^^^^^
+
+The module sets the following variables while it's checking its prerequisites.
+
+.. cmake:variable:: GIT_COMMAND
+
+Path of git executable.
+
+.. cmake:variable:: CLANG_LIBRARY_PATH
+
+c-picker uses libclang to parse the source files. If defined this variable
+specifies the path of the library.
+
+.. cmake:variable:: CPICKER_COMMAND
+
+Path of c-picker executable which is part of the c-picker pip package.
+
+.. cmake:variable:: UNIT_TEST_PROJECT_PATH
+
+Path of the project source directory. **This needs to be specified
+by the developer** to point to a suitable working copy of project to be tested.
+
+.. cmake:variable:: CPPUTEST_URL
+
+URL of the CppUTest git repository. By default it points to the official Github
+repository of CppUTest. It can be used to specify a different CppUTest mirror.
+
+.. cmake:variable:: CPPUTEST_REFSPEC
+
+CppUTest git refspec. The default value selects the latest release.
+
+.. cmake:variable:: CPPUTEST_INSTALL_PATH
+
+Temporary directory used during CppUTest build
+
+.. cmake:variable:: CPPUTEST_PACKAGE_PATH
+
+Path of the CppUTest CMake package directory
+
+.. cmake:variable:: CPICKER_CACHE_PATH
+
+Directory of c-picker generated files. Subdirectories are added according to
+the path of the original source file's path.
+
+.. cmake:variable:: UNIT_TEST_COMMON_SOURCES
+
+Lists of source files that are included in all test builds.
+
+
+Functions
+^^^^^^^^^
+
+#]===]
+
+include_guard(DIRECTORY)
+
+include(FetchContent)
+
+set(CLANG_LIBRARY_PATH_HELP "libclang directory for c-picker")
+
+set(CPPUTEST_URL "https://github.com/cpputest/cpputest.git" CACHE STRING "CppUTest repository URL")
+set(CPPUTEST_REFSPEC "v4.0" CACHE STRING "CppUTest git refspec")
+set(CPPUTEST_INSTALL_PATH ${CMAKE_CURRENT_BINARY_DIR}/CppUTest_install CACHE PATH "CppUTest installation directory")
+set(CPPUTEST_PACKAGE_PATH ${CPPUTEST_INSTALL_PATH}/lib/CppUTest/cmake CACHE PATH "CppUTest CMake package directory")
+
+set(CPICKER_CACHE_PATH ${CMAKE_CURRENT_BINARY_DIR}/cpicker_cache CACHE PATH "Directory of c-picker generated file")
+
+set(UNIT_TEST_COMMON_SOURCES ${CMAKE_CURRENT_LIST_DIR}/../common/main.cpp CACHE STRING "List of common test source files")
+
+# Checking git
+find_program(GIT_COMMAND "git")
+if (NOT GIT_COMMAND)
+	message(FATAL_ERROR "Please install git")
+endif()
+
+if (DEFINED CLANG_LIBRARY_PATH)
+	message(STATUS "Using CLANG_LIBRARY_PATH from CMake variable (command line or cache)")
+
+	if (DEFINED ENV{CLANG_LIBRARY_PATH})
+		if (NOT (${CLANG_LIBRARY_PATH} STREQUAL $ENV{CLANG_LIBRARY_PATH}))
+			message(WARNING "Both CLANG_LIBRARY_PATH CMake and environment variables are set but have different values")
+		endif()
+	endif()
+else()
+	if (DEFINED ENV{CLANG_LIBRARY_PATH})
+		message(STATUS "Setting CLANG_LIBRARY_PATH based on environment variable")
+		set(CLANG_LIBRARY_PATH $ENV{CLANG_LIBRARY_PATH} CACHE PATH ${CLANG_LIBRARY_PATH_HELP})
+	else()
+		message(STATUS "Setting CLANG_LIBRARY_PATH based on find_package")
+		find_package(LibClang REQUIRED)
+		set(CLANG_LIBRARY_PATH ${LibClang_LIBRARY_DIRS} CACHE PATH ${CLANG_LIBRARY_PATH_HELP})
+	endif()
+endif()
+
+message(STATUS "CLANG_LIBRARY_PATH has been set to ${CLANG_LIBRARY_PATH}")
+
+# Checking c-picker
+find_program(CPICKER_COMMAND "c-picker")
+if (NOT CPICKER_COMMAND)
+	message(FATAL_ERROR "Please install c-picker using pip")
+endif()
+
+#[===[.rst:
+.. cmake:command:: unit_test_init_cpputest
+
+  .. code-block:: cmake
+
+    unit_test_init_cpputest()
+
+  The ``unit_test_init_cpputest`` CMake function fetches and build CppUTest unit testing framework.
+  It also enables linking the library to the test binaries.
+
+  Global dependencies:
+
+  ``CPPUTEST_URL``
+    Root directory of the c-picker generated files
+
+  ``CPPUTEST_REFSPEC``
+    Common source files for every test build
+
+#]===]
+function(unit_test_init_cpputest)
+	# Fetching CppUTest
+	FetchContent_Declare(
+		cpputest
+		GIT_REPOSITORY ${CPPUTEST_URL}
+		GIT_TAG ${CPPUTEST_REFSPEC}
+		GIT_SHALLOW TRUE
+		PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/common/cpputest-cmake-fix.patch || true
+	)
+
+	# FetchContent_GetProperties exports cpputest_SOURCE_DIR and cpputest_BINARY_DIR variables
+	FetchContent_GetProperties(cpputest)
+	if(NOT cpputest_POPULATED)
+		message(STATUS "Fetching CppUTest")
+		FetchContent_Populate(cpputest)
+	endif()
+
+	# Build and install CppUTest in CMake time. This makes us able to use CppUTest as a CMake package later.
+	# Memory leak detection is turned off to avoid conflict with memcheck.
+	execute_process(COMMAND
+		${CMAKE_COMMAND}
+			-DMEMORY_LEAK_DETECTION=OFF
+			-DLONGLONG=ON
+			-DC++11=ON
+			-DCMAKE_INSTALL_PREFIX=${CPPUTEST_INSTALL_PATH}
+			-GUnix\ Makefiles
+			${cpputest_SOURCE_DIR}
+		WORKING_DIRECTORY
+			${cpputest_BINARY_DIR}
+	)
+	execute_process(COMMAND ${CMAKE_COMMAND} --build ${cpputest_BINARY_DIR} -- install -j)
+
+	# Finding CppUTest package. CMake will check [package name]_DIR variable.
+	set(CppUTest_DIR ${CPPUTEST_PACKAGE_PATH} CACHE PATH "Path of CppUTestConfig.cmake")
+	find_package(CppUTest CONFIG REQUIRED)
+
+	# find_package sets the CppUTest_INCLUDE_DIRS and CppUTest_LIBRARIES variables
+	include_directories(${CppUTest_INCLUDE_DIRS})
+	link_libraries(${CppUTest_LIBRARIES})
+endfunction()
+
+#[===[.rst:
+.. cmake:command:: unit_test_add_suite
+
+  .. code-block:: cmake
+
+    unit_test_add_suite(
+    	NAME test_name
+    	SOURCES source_files
+    	INCLUDE_DIRECTORIES include_directories
+    	COMPILE_DEFINITIONS defines
+    	DEPENDS dependencies
+    )
+
+  The ``unit_test_add_suite`` CMake function provides a convenient interface for
+  defining unit test suites. Basically its input is the test source files, include
+  paths and macro definitions and it internally does all the necessary steps to
+  have the test binary registered in CTest as a result.
+
+  Control flow:
+
+  1. Adding new executable named ``NAME``
+
+  2. Iterating throught ``SOURCES``
+
+     1. If it's a normal source file add to the executable's source list
+     2. If it's a YAML file add as a c-picker custom command and add the generated
+        file to the executable's source list
+
+  3. Setting include directories
+
+  4. Setting defines
+
+  5. Adding extra dependencies of the test build
+
+  6. Adding executable to the system as a test
+
+  Inputs:
+
+  ``NAME``
+    Unique name of the test suite
+
+  ``SOURCES`` (multi, optional)
+    Source files
+
+  ``INCLUDE_DIRECTORIES`` (multi, optional)
+    Include directories
+
+  ``COMPILE_DEFINITIONS`` (multi, optional)
+    Defines
+
+  ``DEPENDS`` (multi, optional)
+    Extra targets as dependencies of the test build
+
+  Global dependencies:
+
+  ``UNIT_TEST_PROJECT_PATH``
+    Root directory of the project under test.
+
+  ``CPICKER_CACHE_PATH``
+    Root directory of the c-picker generated files
+
+  ``UNIT_TEST_COMMON_SOURCES``
+    Common source files for every test build
+
+  ``CTest``
+    Built-in testing module of CMake
+
+#]===]
+function(unit_test_add_suite)
+	set(_OPTIONS_ARGS)
+	set(_ONE_VALUE_ARGS NAME)
+	set(_MULTI_VALUE_ARGS SOURCES INCLUDE_DIRECTORIES COMPILE_DEFINITIONS DEPENDS)
+	cmake_parse_arguments(_MY_PARAMS "${_OPTIONS_ARGS}" "${_ONE_VALUE_ARGS}" "${_MULTI_VALUE_ARGS}" ${ARGN})
+
+	if(NOT DEFINED BUILD_TESTING)
+		message(FATAL_ERROR
+			"unit_test_add_suite(): "
+			"CTest module should be included in the root CMakeLists.txt before calling this function.")
+	endif()
+
+	if (NOT UNIT_TEST_PROJECT_PATH)
+		message(FATAL_ERROR "UNIT_TEST_PROJECT_PATH is not set")
+	endif()
+
+	set(TEST_NAME ${_MY_PARAMS_NAME})
+	set(TEST_INCLUDE_DIRECTORIES ${_MY_PARAMS_INCLUDE_DIRECTORIES})
+	set(TEST_COMPILE_DEFINITIONS ${_MY_PARAMS_COMPILE_DEFINITIONS})
+	set(TEST_DEPENDS ${_MY_PARAMS_DEPENDS})
+
+	add_executable(${TEST_NAME} ${UNIT_TEST_COMMON_SOURCES})
+
+	foreach(TEST_SOURCE ${_MY_PARAMS_SOURCES})
+		get_filename_component(TEST_SOURCE_EXTENSION ${TEST_SOURCE} EXT)
+
+		if (${TEST_SOURCE_EXTENSION} STREQUAL ".yml")
+			# Building output file name: tests/a/b/test.yml -> ${CPICKER_CACHE_PATH}/a/b/test.c
+			get_filename_component(TEST_SOURCE_DIR ${TEST_SOURCE} DIRECTORY)
+			file(RELATIVE_PATH CPICKER_SOURCE_DIR ${UNIT_TEST_PROJECT_PATH} ${TEST_SOURCE_DIR})
+			get_filename_component(TEST_SOURCE_NAME ${TEST_SOURCE} NAME_WE)
+			set(CPICKER_OUTPUT ${CPICKER_CACHE_PATH}/${TEST_NAME}/${CPICKER_SOURCE_DIR}/${TEST_SOURCE_NAME}.c)
+
+			# Creating output directory
+			get_filename_component(OUTPUT_DIRECTORY ${CPICKER_OUTPUT} DIRECTORY)
+			file(MAKE_DIRECTORY ${OUTPUT_DIRECTORY})
+
+			# Fetching referenced source files as the dependencies of the generated file
+			execute_process(
+				COMMAND
+					${CMAKE_COMMAND} -E env CLANG_LIBRARY_PATH=${CLANG_LIBRARY_PATH}
+					${CPICKER_COMMAND} --config ${TEST_SOURCE} --root ${UNIT_TEST_PROJECT_PATH} --print-dependencies
+				OUTPUT_VARIABLE CPICKER_DEPENDENCIES
+			)
+
+			# Adding custom command for invoking c-picker
+			add_custom_command(
+				OUTPUT ${CPICKER_OUTPUT}
+				COMMAND
+					${CMAKE_COMMAND} -E env CLANG_LIBRARY_PATH=${CLANG_LIBRARY_PATH}
+					${CPICKER_COMMAND} --config ${TEST_SOURCE} --root ${UNIT_TEST_PROJECT_PATH} > ${CPICKER_OUTPUT}
+				DEPENDS ${TEST_SOURCE} ${CPICKER_DEPENDENCIES}
+				COMMENT "Generating c-picker output ${CPICKER_OUTPUT}"
+			)
+			set(TEST_SOURCE ${CPICKER_OUTPUT})
+		endif()
+
+		target_sources(${TEST_NAME} PRIVATE ${TEST_SOURCE})
+	endforeach()
+
+	target_include_directories(${TEST_NAME} PRIVATE ${TEST_INCLUDE_DIRECTORIES})
+	target_compile_definitions(${TEST_NAME} PRIVATE ${TEST_COMPILE_DEFINITIONS})
+	if (TEST_DEPENDS)
+		add_dependencies(${TEST_NAME} ${TEST_DEPENDS})
+	endif()
+	add_test(${TEST_NAME} ${TEST_NAME})
+endfunction()