From 198a7a9060d68c294c7c9199fe65956448b50b69 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 22 May 2026 07:41:24 -0400 Subject: [PATCH] xo-testutil: unit test support library --- CMakeLists.txt | 1 + README.md | 1 + default.nix | 2 + pkgs/xo-testutil.nix | 55 ++++++++++++++ xo-testutil/CMakeLists.txt | 30 ++++++++ xo-testutil/cmake/xo-bootstrap-macros.cmake | 33 ++++++++ xo-testutil/cmake/xo_testutilConfig.cmake.in | 14 ++++ xo-testutil/include/xo/testutil/Utest.hpp | 35 +++++++++ .../include/xo/testutil/UtestAppStart.hpp | 30 ++++++++ .../include/xo/testutil/UtestConfig.hpp | 25 +++++++ .../include/xo/testutil/UtestListener.hpp | 52 +++++++++++++ xo-testutil/src/testutil/CMakeLists.txt | 15 ++++ xo-testutil/src/testutil/Utest.cpp | 18 +++++ xo-testutil/src/testutil/UtestAppStart.cpp | 75 +++++++++++++++++++ xo-testutil/src/testutil/UtestConfig.cpp | 18 +++++ 15 files changed, 404 insertions(+) create mode 100644 pkgs/xo-testutil.nix create mode 100644 xo-testutil/CMakeLists.txt create mode 100644 xo-testutil/cmake/xo-bootstrap-macros.cmake create mode 100644 xo-testutil/cmake/xo_testutilConfig.cmake.in create mode 100644 xo-testutil/include/xo/testutil/Utest.hpp create mode 100644 xo-testutil/include/xo/testutil/UtestAppStart.hpp create mode 100644 xo-testutil/include/xo/testutil/UtestConfig.hpp create mode 100644 xo-testutil/include/xo/testutil/UtestListener.hpp create mode 100644 xo-testutil/src/testutil/CMakeLists.txt create mode 100644 xo-testutil/src/testutil/Utest.cpp create mode 100644 xo-testutil/src/testutil/UtestAppStart.cpp create mode 100644 xo-testutil/src/testutil/UtestConfig.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 17b6cafb..d143429d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ add_subdirectory(xo-facet) # sep iface,data add_subdirectory(xo-allocutil) add_subdirectory(xo-refcnt) add_subdirectory(xo-subsys) +add_subdirectory(xo-testutil) # unit test aux functions add_subdirectory(xo-flatstring) add_subdirectory(xo-pyutil) add_subdirectory(xo-reflect) diff --git a/README.md b/README.md index 9b921011..7468ae11 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ using `xo-build`. Finally, can also individual XO packages: ``` $ nix-build -A xo.cmake +$ nix-build -A xo.indentlog ... ``` diff --git a/default.nix b/default.nix index 3aff84df..ec625b4d 100644 --- a/default.nix +++ b/default.nix @@ -148,6 +148,7 @@ let xo-allocutil = self.callPackage pkgs/xo-allocutil.nix { stdenv = jitStdenv; }; xo-refcnt = self.callPackage pkgs/xo-refcnt.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-pyutil = self.callPackage pkgs/xo-pyutil.nix { stdenv = jitStdenv; }; xo-reflect = self.callPackage pkgs/xo-reflect.nix { stdenv = jitStdenv; }; @@ -526,6 +527,7 @@ in allocutil = pkgs.xo-allocutil; refcnt = pkgs.xo-refcnt; subsys = pkgs.xo-subsys; + testutil = pkgs.xo-testutil; flatstring = pkgs.xo-flatstring; pyutil = pkgs.xo-pyutil; reflect = pkgs.xo-reflect; diff --git a/pkgs/xo-testutil.nix b/pkgs/xo-testutil.nix new file mode 100644 index 00000000..bcc79584 --- /dev/null +++ b/pkgs/xo-testutil.nix @@ -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 + ]; + }) diff --git a/xo-testutil/CMakeLists.txt b/xo-testutil/CMakeLists.txt new file mode 100644 index 00000000..6a912ee8 --- /dev/null +++ b/xo-testutil/CMakeLists.txt @@ -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 diff --git a/xo-testutil/cmake/xo-bootstrap-macros.cmake b/xo-testutil/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..2cf387e5 --- /dev/null +++ b/xo-testutil/cmake/xo-bootstrap-macros.cmake @@ -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() diff --git a/xo-testutil/cmake/xo_testutilConfig.cmake.in b/xo-testutil/cmake/xo_testutilConfig.cmake.in new file mode 100644 index 00000000..e7ffa898 --- /dev/null +++ b/xo-testutil/cmake/xo_testutilConfig.cmake.in @@ -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@") diff --git a/xo-testutil/include/xo/testutil/Utest.hpp b/xo-testutil/include/xo/testutil/Utest.hpp new file mode 100644 index 00000000..e8516b36 --- /dev/null +++ b/xo-testutil/include/xo/testutil/Utest.hpp @@ -0,0 +1,35 @@ +/** @file Utest.hpp + * + * @author Roland Conybeare, May 2026 + **/ + +#pragma once + +#include + +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 */ diff --git a/xo-testutil/include/xo/testutil/UtestAppStart.hpp b/xo-testutil/include/xo/testutil/UtestAppStart.hpp new file mode 100644 index 00000000..3cbe683d --- /dev/null +++ b/xo-testutil/include/xo/testutil/UtestAppStart.hpp @@ -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 */ diff --git a/xo-testutil/include/xo/testutil/UtestConfig.hpp b/xo-testutil/include/xo/testutil/UtestConfig.hpp new file mode 100644 index 00000000..9c8179ec --- /dev/null +++ b/xo-testutil/include/xo/testutil/UtestConfig.hpp @@ -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 */ diff --git a/xo-testutil/include/xo/testutil/UtestListener.hpp b/xo-testutil/include/xo/testutil/UtestListener.hpp new file mode 100644 index 00000000..a76d50fb --- /dev/null +++ b/xo-testutil/include/xo/testutil/UtestListener.hpp @@ -0,0 +1,52 @@ +/** @file UtestListener.hpp + * + * @author Roland Conybeare, May 2026 + **/ + +#pragma once + +#include "UtestConfig.hpp" + +// caller must define CATCH_CONFIG_RUNNER +#include + +namespace xo { + + /** @brief listener for catch2 unit tests. + * catch2 invokes this at the beginning of each unit test + * + * Enable with: + * @begin_code + * #include + * 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 */ diff --git a/xo-testutil/src/testutil/CMakeLists.txt b/xo-testutil/src/testutil/CMakeLists.txt new file mode 100644 index 00000000..b43b6144 --- /dev/null +++ b/xo-testutil/src/testutil/CMakeLists.txt @@ -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) diff --git a/xo-testutil/src/testutil/Utest.cpp b/xo-testutil/src/testutil/Utest.cpp new file mode 100644 index 00000000..c0632819 --- /dev/null +++ b/xo-testutil/src/testutil/Utest.cpp @@ -0,0 +1,18 @@ +/** @file Utest.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "Utest.hpp" +#include "UtestConfig.hpp" +#include + +namespace xo { + scope + Utest::ut_scope() { + return scope(XO_DEBUG(UtestConfig::instance()->debug_flag()), + xtag("name", Catch::getResultCapture().getCurrentTestName())); + } +} + +/* end Utest.cpp */ diff --git a/xo-testutil/src/testutil/UtestAppStart.cpp b/xo-testutil/src/testutil/UtestAppStart.cpp new file mode 100644 index 00000000..433c76a1 --- /dev/null +++ b/xo-testutil/src/testutil/UtestAppStart.cpp @@ -0,0 +1,75 @@ +/** @file UtestAppStart.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "UtestAppStart.hpp" +#include "UtestConfig.hpp" +#include +#include +#include + +#define CATCH_CONFIG_RUNNER +#include + +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 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 */ diff --git a/xo-testutil/src/testutil/UtestConfig.cpp b/xo-testutil/src/testutil/UtestConfig.cpp new file mode 100644 index 00000000..49bd2c4c --- /dev/null +++ b/xo-testutil/src/testutil/UtestConfig.cpp @@ -0,0 +1,18 @@ +/** @file UtestConfig.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "UtestConfig.hpp" +#include + +namespace xo { + UtestConfig * + UtestConfig::instance() { + static UtestConfig s_instance; + + return &s_instance; + } +} + +/* end UtestConfig.cpp */