xo-testutil: unit test support library

This commit is contained in:
Roland Conybeare 2026-05-22 07:41:24 -04:00
commit 198a7a9060
15 changed files with 404 additions and 0 deletions

View file

@ -87,6 +87,7 @@ add_subdirectory(xo-facet) # sep iface,data
add_subdirectory(xo-allocutil) add_subdirectory(xo-allocutil)
add_subdirectory(xo-refcnt) add_subdirectory(xo-refcnt)
add_subdirectory(xo-subsys) add_subdirectory(xo-subsys)
add_subdirectory(xo-testutil) # unit test aux functions
add_subdirectory(xo-flatstring) add_subdirectory(xo-flatstring)
add_subdirectory(xo-pyutil) add_subdirectory(xo-pyutil)
add_subdirectory(xo-reflect) add_subdirectory(xo-reflect)

View file

@ -164,6 +164,7 @@ using `xo-build`.
Finally, can also individual XO packages: Finally, can also individual XO packages:
``` ```
$ nix-build -A xo.cmake $ nix-build -A xo.cmake
$ nix-build -A xo.indentlog
... ...
``` ```

View file

@ -148,6 +148,7 @@ let
xo-allocutil = self.callPackage pkgs/xo-allocutil.nix { stdenv = jitStdenv; }; xo-allocutil = self.callPackage pkgs/xo-allocutil.nix { stdenv = jitStdenv; };
xo-refcnt = self.callPackage pkgs/xo-refcnt.nix { stdenv = jitStdenv; }; xo-refcnt = self.callPackage pkgs/xo-refcnt.nix { stdenv = jitStdenv; };
xo-subsys = self.callPackage pkgs/xo-subsys.nix { stdenv = jitStdenv; }; xo-subsys = self.callPackage pkgs/xo-subsys.nix { stdenv = jitStdenv; };
xo-testutil = self.callPackage pkgs/xo-testutil.nix { stdenv = jitStdenv; };
xo-flatstring = self.callPackage pkgs/xo-flatstring.nix { stdenv = jitStdenv; buildDocs = true; buildExamples = true; }; xo-flatstring = self.callPackage pkgs/xo-flatstring.nix { stdenv = jitStdenv; buildDocs = true; buildExamples = true; };
xo-pyutil = self.callPackage pkgs/xo-pyutil.nix { stdenv = jitStdenv; }; xo-pyutil = self.callPackage pkgs/xo-pyutil.nix { stdenv = jitStdenv; };
xo-reflect = self.callPackage pkgs/xo-reflect.nix { stdenv = jitStdenv; }; xo-reflect = self.callPackage pkgs/xo-reflect.nix { stdenv = jitStdenv; };
@ -526,6 +527,7 @@ in
allocutil = pkgs.xo-allocutil; allocutil = pkgs.xo-allocutil;
refcnt = pkgs.xo-refcnt; refcnt = pkgs.xo-refcnt;
subsys = pkgs.xo-subsys; subsys = pkgs.xo-subsys;
testutil = pkgs.xo-testutil;
flatstring = pkgs.xo-flatstring; flatstring = pkgs.xo-flatstring;
pyutil = pkgs.xo-pyutil; pyutil = pkgs.xo-pyutil;
reflect = pkgs.xo-reflect; reflect = pkgs.xo-reflect;

55
pkgs/xo-testutil.nix Normal file
View file

@ -0,0 +1,55 @@
{
# nixpkgs dependencies
lib, stdenv, cmake, catch2,
doxygen, cli11,
python3Packages,
sphinx, graphviz,
# xo dependencies
xo-subsys,
xo-indentlog,
xo-cmake,
buildDocs ? false,
doCheck ? true,
} :
stdenv.mkDerivation (finalattrs:
{
name = "xo-testutil";
src = ../xo-testutil;
cmakeFlags = ["-DCMAKE_MODULE_PATH=${xo-cmake}/share/cmake"]
++ lib.optionals buildDocs ["-DXO_ENABLE_DOCS=on"]
++ lib.optionals doCheck ["-DENABLE_TESTING=1"];
inherit buildDocs;
inherit doCheck;
postBuild = lib.optionalString buildDocs ''
cmake --build . -- docs
'';
nativeBuildInputs = [
cmake
catch2
cli11
xo-cmake
] ++ lib.optionals buildDocs [
doxygen
sphinx
graphviz
python3Packages.sphinx-rtd-theme
python3Packages.breathe
python3Packages.sphinxcontrib-ditaa
python3Packages.sphinxcontrib-plantuml
python3Packages.pillow
];
propagatedBuildInputs = [
xo-subsys
xo-indentlog
];
})

View file

@ -0,0 +1,30 @@
# xo-testutil/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(xo_testutil VERSION 1.0)
enable_language(CXX)
include(GNUInstallDirs)
include(cmake/xo-bootstrap-macros.cmake)
xo_cxx_toplevel_options3()
# ----------------------------------------------------------------
# c++ settings
# one-time project-specific c++ flags. usually empty
set(PROJECT_CXX_FLAGS "")
add_definitions(${PROJECT_CXX_FLAGS})
# ----------------------------------------------------------------
# output targets
add_subdirectory(src/testutil)
# ----------------------------------------------------------------
# cmake export
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
# end CMakeLists.txt

View file

@ -0,0 +1,33 @@
# ----------------------------------------------------------------
# for example:
# $ PREFIX=/usr/local # for example
# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build
#
# will get
# CMAKE_MODULE_PATH
# from xo-cmake-config --cmake-module-path
#
# and expect .cmake macros in
# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake
# ----------------------------------------------------------------
find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED)
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL "prefix"))
message(FATAL "could not find xo-cmake-config executable")
endif()
if (NOT XO_SUBMODULE_BUILD)
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
# default to typical install location for xo-project-macros
execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH)
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
endif()
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
#
include(xo_macros/xo_cxx)
xo_cxx_bootstrap_message()

View file

@ -0,0 +1,14 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# note: changes to find_dependency() calls here
# must coordinate with xo_dependency() calls
# in CMakeLists.txt
#
find_dependency(subsys)
find_dependency(indentlog)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake")
check_required_components("@PROJECT_NAME@")

View file

@ -0,0 +1,35 @@
/** @file Utest.hpp
*
* @author Roland Conybeare, May 2026
**/
#pragma once
#include <xo/indentlog/scope.hpp>
namespace xo {
/** RAII logging for catch2 unit tests
*
* Use:
* TEST_CASE(name, tags, ..)
* {
* scope log = Utest::ut_scope();
*
* ...
* log && log(xtag("foo", ...));
* }
*
* Honors:
* UtestConfig::instance()->debug_flag_
**/
struct Utest {
/** Toplevel logging scope for unit tests.
* Integrates with UtestConfig
**/
static scope ut_scope();
};
} /*namespace xo*/
/* end Utest.hpp */

View file

@ -0,0 +1,30 @@
/** @file UtestAppStart.hpp
*
* @author Roland Conybeare, May 2026
**/
#pragma once
namespace xo {
/** @brief Startup sequence for a unit test
*
* Standard unit test startup sequence
**/
class UtestAppStart {
public:
explicit UtestAppStart(const char * app_name) : app_name_{app_name} {}
/**
* Parse program arguments; recognize XO test arguments,
* sending remainder to catch2; do subsystem initialization
**/
int run(int argc, char * argv[]);
private:
const char * app_name_ = "";
};
} /*namespace xo*/
/* end UtestAppStart.hpp */

View file

@ -0,0 +1,25 @@
/** @file UtestConfig.hpp
*
* @author Roland Conybeare, May 2026
**/
namespace xo {
/** unit-test configuration here
*
* TODO: promote to its own library, along with UtestListener
**/
struct UtestConfig {
bool debug_flag() const { return debug_flag_; }
/** announce each test using catch2's listener api **/
bool announce_flag_ = false;
/** enable debug output for all (!) tests **/
bool debug_flag_ = false;
static UtestConfig * instance();
};
}
/* end UtestConfig.hpp */

View file

@ -0,0 +1,52 @@
/** @file UtestListener.hpp
*
* @author Roland Conybeare, May 2026
**/
#pragma once
#include "UtestConfig.hpp"
// caller must define CATCH_CONFIG_RUNNER
#include <catch2/catch.hpp>
namespace xo {
/** @brief listener for catch2 unit tests.
* catch2 invokes this at the beginning of each unit test
*
* Enable with:
* @begin_code
* #include <catch2/catch.hpp>
* CATCH_REGISTER_LISTENER(UtestListener);
* @end_code
**/
struct UtestListener : Catch::TestEventListenerBase {
using TestEventListenerBase::TestEventListenerBase;
// TestCasweInfo members: .name, .className, .description, .tags, lineInfo {.file, .line}
virtual void testCaseStarting(const Catch::TestCaseInfo & info) override {
using std::cerr;
using std::endl;
// preamble
if (UtestConfig::instance()->announce_flag_) {
cerr << "Starting unit test: "
<< "[" << info.name << "]"
<< " at "
<< "[" << info.lineInfo.file << ":" << info.lineInfo.line << "]"
<< endl;
}
}
virtual void testCaseEnded(const Catch::TestCaseStats & stats) override {
// postamble
}
// also sectionStarting / sectionEnded
};
}
/* end UtestListener.hpp */

View file

@ -0,0 +1,15 @@
# testutil/CMakeLists.txt
set(SELF_LIB xo_testutil)
set(SELF_SRCS
UtestAppStart.cpp
UtestConfig.cpp
Utest.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_install_include_tree3(include/xo/testutil)
# note: deps here must also appear in cmake/xo_testutilConfig.cmake.in
xo_dependency(${SELF_LIB} subsys)
xo_dependency(${SELF_LIB} indentlog)
xo_external_target_dependency(${SELF_LIB} Catch2 Catch2::Catch2)

View file

@ -0,0 +1,18 @@
/** @file Utest.cpp
*
* @author Roland Conybeare, May 2026
**/
#include "Utest.hpp"
#include "UtestConfig.hpp"
#include <catch2/catch.hpp>
namespace xo {
scope
Utest::ut_scope() {
return scope(XO_DEBUG(UtestConfig::instance()->debug_flag()),
xtag("name", Catch::getResultCapture().getCurrentTestName()));
}
}
/* end Utest.cpp */

View file

@ -0,0 +1,75 @@
/** @file UtestAppStart.cpp
*
* @author Roland Conybeare, May 2026
**/
#include "UtestAppStart.hpp"
#include "UtestConfig.hpp"
#include <xo/subsys/Subsystem.hpp>
#include <xo/indentlog/scope.hpp>
#include <CLI/CLI.hpp>
#define CATCH_CONFIG_RUNNER
#include <catch2/catch.hpp>
namespace xo {
using xo::UtestConfig;
using xo::scope;
using xo::xtag;
using std::cout;
using std::cerr;
using std::endl;
int
UtestAppStart::run(int argc, char * argv[])
{
CLI::App app{app_name_};
app.set_help_flag(); // disable default help impl, see below
{
app.add_flag("--debug",
UtestConfig::instance()->debug_flag_,
"enable debug logging (for all tests)");
app.add_flag("--announce",
UtestConfig::instance()->announce_flag_,
"announce each test via UtestListener");
}
bool help_flag = false;
{
app.add_flag("--help,-h,-?", help_flag, "print this help message and exit");
}
app.allow_extras();
CLI11_PARSE(app, argc, argv);
std::vector<const char *> argv2 = {argv[0]};
if (help_flag) {
// actual help impl, falls through to Session below
cout << app_name_ << " options" << endl;
cout << app.help() << endl;
cout << "catch2 options" << endl;
argv2.push_back("--help");
} else {
// keep program name
for (auto & x : app.remaining())
argv2.push_back(x.c_str());
}
using xo::Subsystem;
Subsystem::initialize_all();
scope log(XO_DEBUG(UtestConfig::instance()->debug_flag()),
"start catch2 session");
// run catch2's test session / help
return Catch::Session().run(argv2.size(), argv2.data());
}
} /*namespace xo*/
/* end UtestAppStart.cpp */

View file

@ -0,0 +1,18 @@
/** @file UtestConfig.cpp
*
* @author Roland Conybeare, May 2026
**/
#include "UtestConfig.hpp"
#include <catch2/catch.hpp>
namespace xo {
UtestConfig *
UtestConfig::instance() {
static UtestConfig s_instance;
return &s_instance;
}
}
/* end UtestConfig.cpp */