diff --git a/default.nix b/default.nix index 19d55283..3aff84df 100644 --- a/default.nix +++ b/default.nix @@ -328,6 +328,7 @@ let # xo general-purpose devutils devutils = [ pkgs.nix-tree # note: needs GHC + #pkgs.nix pkgs.git pkgs.gh # github cli @@ -335,6 +336,7 @@ let pkgs.bloaty pkgs.catch2 + pkgs.cli11 pkgs.btop ] diff --git a/pkgs/xo-alloc2.nix b/pkgs/xo-alloc2.nix index 0ca4afee..74d86500 100644 --- a/pkgs/xo-alloc2.nix +++ b/pkgs/xo-alloc2.nix @@ -1,7 +1,7 @@ { # nixpkgs dependencies lib, stdenv, cmake, catch2, - doxygen, + doxygen, cli11, python3Packages, @@ -44,6 +44,7 @@ stdenv.mkDerivation (finalattrs: nativeBuildInputs = [ cmake catch2 + cli11 xo-randomgen xo-cmake ] ++ lib.optionals buildDocs [ diff --git a/xo-alloc2/utest/CMakeLists.txt b/xo-alloc2/utest/CMakeLists.txt index a19d1f47..2dec2979 100644 --- a/xo-alloc2/utest/CMakeLists.txt +++ b/xo-alloc2/utest/CMakeLists.txt @@ -4,12 +4,11 @@ set(UTEST_EXE utest.alloc2) set(UTEST_SRCS alloc2_utest_main.cpp + TestUtil.cpp objectmodel.test.cpp arena.test.cpp IAllocator_Any.test.cpp DArenaIterator.test.cpp -# Collector.test.cpp -# DX1CollectorIterator.test.cpp Generation.test.cpp Role.test.cpp VisitReason.test.cpp diff --git a/xo-alloc2/utest/DArenaIterator.test.cpp b/xo-alloc2/utest/DArenaIterator.test.cpp index 2c7503c5..a3e95d96 100644 --- a/xo-alloc2/utest/DArenaIterator.test.cpp +++ b/xo-alloc2/utest/DArenaIterator.test.cpp @@ -3,10 +3,11 @@ * @author Roland Conybeare, Dec 2025 **/ -#include "Allocator.hpp" -#include "AllocIterator.hpp" -#include "Arena.hpp" -//#include "arena/IAllocator_DArena.hpp" +#include "TestUtil.hpp" +#include +#include +#include +#include #include "arena/IAllocIterator_DArenaIterator.hpp" #include "padding.hpp" #include @@ -39,6 +40,8 @@ namespace xo { namespace ut { TEST_CASE("IAllocIterator_Xfer_DArenaIterator", "[alloc2]") { + auto log = Utest::ut_scope(); + /* verify IAllocIterator_Xfer is constructible + satisfies concept checks */ IAllocIterator_Xfer xfer; REQUIRE(IAllocIterator_Xfer::_valid); @@ -46,6 +49,8 @@ namespace xo { TEST_CASE("IAllocIterator_Any", "[alloc2]") { + auto log = Utest::ut_scope(); + /* verify IAllocIterator_Any is constructible + satisfies concept checks */ IAllocIterator_Any any; REQUIRE(IAllocIterator_Any::_valid); @@ -53,6 +58,8 @@ namespace xo { TEST_CASE("obj_IAllocIterator", "[alloc2]") { + auto log = Utest::ut_scope(); + /* verify variant obj constructible */ obj obj_any; REQUIRE(obj_any.iface()); @@ -61,6 +68,8 @@ namespace xo { TEST_CASE("IAllocIterator-disabled-1", "[alloc2]") { + auto log = Utest::ut_scope(); + /* verify iteration over empty arena */ /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", @@ -97,6 +106,8 @@ namespace xo { TEST_CASE("IAllocIterator-emptyarena", "[alloc2]") { + auto log = Utest::ut_scope(); + /* verify iteration over empty arena */ /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", @@ -153,7 +164,7 @@ namespace xo { TEST_CASE("IAllocIterator-singlearena", "[alloc2]") { - scope log(XO_DEBUG(false)); + auto log = Utest::ut_scope(); ArenaConfig cfg { .name_ = "testarena", .size_ = 64*1024, diff --git a/xo-alloc2/utest/Generation.test.cpp b/xo-alloc2/utest/Generation.test.cpp index e62359ea..6507e19b 100644 --- a/xo-alloc2/utest/Generation.test.cpp +++ b/xo-alloc2/utest/Generation.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include "Generation.hpp" #include @@ -14,6 +15,8 @@ namespace xo { TEST_CASE("Generation-1", "[Generation]") { + auto log = Utest::ut_scope(); + REQUIRE(Generation::nursery() == 0); REQUIRE(Generation::g0() == 0); REQUIRE(Generation::g1() == 1); diff --git a/xo-alloc2/utest/IAllocator_Any.test.cpp b/xo-alloc2/utest/IAllocator_Any.test.cpp index 5d23c6ed..68d510ef 100644 --- a/xo-alloc2/utest/IAllocator_Any.test.cpp +++ b/xo-alloc2/utest/IAllocator_Any.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include #include #include @@ -16,6 +17,8 @@ namespace xo { TEST_CASE("IAllocator_Any", "[alloc2][death]") { + auto log = Utest::ut_scope(); + // null allocator obj alloc_any; diff --git a/xo-alloc2/utest/ResourceVisitor.test.cpp b/xo-alloc2/utest/ResourceVisitor.test.cpp index 0869d387..49c0f553 100644 --- a/xo-alloc2/utest/ResourceVisitor.test.cpp +++ b/xo-alloc2/utest/ResourceVisitor.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include #include @@ -13,6 +14,8 @@ namespace xo { TEST_CASE("ResourceVisitor-1", "[resourcevisitor]") { + auto log = Utest::ut_scope(); + obj v; REQUIRE(v.iface()); diff --git a/xo-alloc2/utest/Role.test.cpp b/xo-alloc2/utest/Role.test.cpp index d1de6fe4..a4c38d29 100644 --- a/xo-alloc2/utest/Role.test.cpp +++ b/xo-alloc2/utest/Role.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include "role.hpp" #include @@ -13,6 +14,8 @@ namespace xo { TEST_CASE("Role-1", "[Role]") { + auto log = Utest::ut_scope(); + /* 1. there are two distinct valid roles, 'to' and 'from', * 2. valid roles fall in interval [begin, end) */ diff --git a/xo-alloc2/utest/TestUtil.cpp b/xo-alloc2/utest/TestUtil.cpp new file mode 100644 index 00000000..c52a5d79 --- /dev/null +++ b/xo-alloc2/utest/TestUtil.cpp @@ -0,0 +1,24 @@ +/** @file TestUtil.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "TestUtil.hpp" +#include + +namespace xo { + UtestConfig * + UtestConfig::instance() { + static UtestConfig s_instance; + + return &s_instance; + }; + + scope + Utest::ut_scope() { + return scope(XO_DEBUG(UtestConfig::instance()->debug_flag()), + xtag("name", Catch::getResultCapture().getCurrentTestName())); + } +} + +/* end TestUtil.cpp */ diff --git a/xo-alloc2/utest/TestUtil.hpp b/xo-alloc2/utest/TestUtil.hpp new file mode 100644 index 00000000..20253bb6 --- /dev/null +++ b/xo-alloc2/utest/TestUtil.hpp @@ -0,0 +1,44 @@ +/** @file TestUtil.hpp + * + * @author Roland Conybeare, May 2026 + **/ + +#pragma once + +#include + +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(); + }; + + /** RAII logging for catch unit tests + * + * Use: + * TEST_CASE(name, tags, ..) + * { + * scope log = Utest::ut_scope(); + * + * ... + * log && log(xtag("foo", ...)); + * } + **/ + struct Utest { + static scope ut_scope(); + }; + +} /*namespace xo*/ + +/* end TestUtil.hpp */ diff --git a/xo-alloc2/utest/VisitReason.test.cpp b/xo-alloc2/utest/VisitReason.test.cpp index 44bfcc38..6fccac80 100644 --- a/xo-alloc2/utest/VisitReason.test.cpp +++ b/xo-alloc2/utest/VisitReason.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include #include @@ -13,6 +14,8 @@ namespace xo { TEST_CASE("VisitReason-1", "[visitreason]") { + auto log = Utest::ut_scope(); + REQUIRE(VisitReason::unspecified() == VisitReason::unspecified()); REQUIRE(VisitReason::unspecified() != VisitReason::forward()); diff --git a/xo-alloc2/utest/alloc2_utest_main.cpp b/xo-alloc2/utest/alloc2_utest_main.cpp index 02d64a23..d907b0d9 100644 --- a/xo-alloc2/utest/alloc2_utest_main.cpp +++ b/xo-alloc2/utest/alloc2_utest_main.cpp @@ -1,6 +1,100 @@ /* file alloc2_utest_main.cpp */ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" +#include "TestUtil.hpp" +#include +#include +#include + +#define CATCH_CONFIG_RUNNER +#include + +namespace xo { + + 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 + + }; + + CATCH_REGISTER_LISTENER(UtestListener); +} + +int +main(int argc, char* argv[]) +{ + using xo::UtestConfig; + using xo::scope; + using xo::xtag; + + using std::cout; + using std::cerr; + using std::endl; + + //cerr << xtag("cli11", CLI11_VERSION) << endl; // version 2.5.0 + + CLI::App app{"utest.alloc2: xo-alloc2 unit tests"}; + 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 << "utest.alloc2 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()); +} /* end alloc2_utest_main.cpp */ diff --git a/xo-alloc2/utest/arena.test.cpp b/xo-alloc2/utest/arena.test.cpp index 95077f6b..4f88ffd2 100644 --- a/xo-alloc2/utest/arena.test.cpp +++ b/xo-alloc2/utest/arena.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, Dec 2025 **/ +#include "TestUtil.hpp" #include #include #include @@ -33,6 +34,8 @@ namespace xo { namespace ut { TEST_CASE("IAllocator_Xfer_DArena", "[alloc2]") { + auto log = Utest::ut_scope(); + IAllocator_Xfer xfer; REQUIRE(IAllocator_Xfer::_valid); @@ -40,6 +43,8 @@ namespace xo { TEST_CASE("DArena-medium", "[alloc2][DArena]") { + auto log = Utest::ut_scope(); + ArenaConfig cfg { .name_ = "testarena", .size_ = 10*1024*1024 }; DArena arena = DArena::map(cfg); @@ -83,6 +88,8 @@ namespace xo { TEST_CASE("allocator-any-1", "[alloc2][AAllocator]") { + auto log = Utest::ut_scope(); + /* empty allocator alloc1 */ obj alloc1; @@ -122,6 +129,8 @@ namespace xo { TEST_CASE("allocator-expand-1", "[alloc2][AAllocator]") { + auto log = Utest::ut_scope(); + /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", .size_ = 1, @@ -160,6 +169,8 @@ namespace xo { TEST_CASE("allocator-alloc-1", "[alloc2][AAllocator]") { + auto log = Utest::ut_scope(); + /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", .size_ = 64*1024, @@ -207,6 +218,8 @@ namespace xo { TEST_CASE("allocator-alloc-2", "[alloc2][Allocator]") { + auto log = Utest::ut_scope(); + using header_type = AllocHeader; /* typed allocator a1o, with object header */ @@ -293,6 +306,8 @@ namespace xo { TEST_CASE("allocator-alloc-3", "[alloc2][Allocator]") { + auto log = Utest::ut_scope(); + using header_type = AllocHeader; /* typed allocator a1o, with object header + guard bytes */ @@ -363,6 +378,8 @@ namespace xo { TEST_CASE("allocator-fail-1", "[alloc2][AAllocator]") { + auto log = Utest::ut_scope(); + /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", .size_ = 64*1024, diff --git a/xo-alloc2/utest/dp.test.cpp b/xo-alloc2/utest/dp.test.cpp index e507b730..c2c91d70 100644 --- a/xo-alloc2/utest/dp.test.cpp +++ b/xo-alloc2/utest/dp.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, May 2026 **/ +#include "TestUtil.hpp" #include "dp.hpp" #include #include @@ -33,6 +34,8 @@ namespace xo { TEST_CASE("dp-1", "[dp]") { + auto log = Utest::ut_scope(); + //ArenaConfig cfg { .name_ = "testarena", .size_ = 1024 }; //DArena arena = DArena::map(cfg); //auto mm = obj(&arena); @@ -55,6 +58,8 @@ namespace xo { TEST_CASE("dp-2", "[dp]") { + auto log = Utest::ut_scope(); + uint32_t counter = 0; Foo foo(&counter); @@ -77,6 +82,8 @@ namespace xo { TEST_CASE("dp-DArena", "[dp][DArena]") { + auto log = Utest::ut_scope(); + ArenaConfig cfg { .name_ = "testarena", .size_ = 1024 }; DArena arena = DArena::map(cfg); //auto mm = obj(&arena); diff --git a/xo-alloc2/utest/objectmodel.test.cpp b/xo-alloc2/utest/objectmodel.test.cpp index 1fcc9a81..5f08b2c8 100644 --- a/xo-alloc2/utest/objectmodel.test.cpp +++ b/xo-alloc2/utest/objectmodel.test.cpp @@ -79,6 +79,7 @@ * Application code will deal with ubox **/ +#include "TestUtil.hpp" #include #include #include @@ -576,6 +577,8 @@ namespace xo { TEST_CASE("objectmodel-specific-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + /* arg=0, mag=1 -> 1+0i */ DPolarCoords polar{0.0, 1.0}; IComplex_Specific polar_iface; @@ -588,6 +591,8 @@ namespace xo { TEST_CASE("objectmodel-specific-2", "[objectmodel]") { + auto log = Utest::ut_scope(); + /* arg=0, mag=1 -> 1+0i */ DRectCoords rect{1.0, 0.0}; IComplex_Specific rect_iface; @@ -600,6 +605,8 @@ namespace xo { TEST_CASE("uniquebox-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + auto tmp = std::make_unique(0.0, 1.0); OUniqueBox box{tmp.release()}; @@ -611,6 +618,8 @@ namespace xo { TEST_CASE("router-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + using Object = OUniqueBox; auto tmp = std::make_unique(0.0, 1.0); @@ -624,6 +633,8 @@ namespace xo { TEST_CASE("routing-type-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + using Object = OUniqueBox; auto tmp = std::make_unique(0.0, 1.0); @@ -637,6 +648,8 @@ namespace xo { TEST_CASE("ubox-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + auto tmp = std::make_unique(0.0, 1.0); ubox box{tmp.release()}; @@ -648,6 +661,8 @@ namespace xo { TEST_CASE("ubox-2", "[objectmodel]") { + auto log = Utest::ut_scope(); + auto tmp = std::make_unique(1.0, 0.0); ubox box{tmp.release()}; @@ -659,12 +674,16 @@ namespace xo { TEST_CASE("ubox-any-1", "[objectmodel]") { + auto log = Utest::ut_scope(); + /* default ctor */ ubox any; } TEST_CASE("ubox-any-2", "[objectmodel]") { + auto log = Utest::ut_scope(); + /* equivalent to ubox, but impl doesn't use std::unique_ptr */ ubox any{new DRectCoords{1.0, 0.0}}; @@ -676,6 +695,8 @@ namespace xo { TEST_CASE("ubox-any-3", "[objectmodel]") { + auto log = Utest::ut_scope(); + /* equivalent to ubox, but impl doesn't use std::unique_ptr */ ubox z1{new DRectCoords{1.0, 0.0}}; diff --git a/xo-arena/src/arena/DArena.cpp b/xo-arena/src/arena/DArena.cpp index fb60031a..0c429558 100644 --- a/xo-arena/src/arena/DArena.cpp +++ b/xo-arena/src/arena/DArena.cpp @@ -538,9 +538,9 @@ namespace xo { if (lo_ + target_z > hi_) [[unlikely]] { this->capture_error(error::reserve_exhausted, src_fn, target_z); - fprintf(stderr, "DArena::expand: reserve exhausted"); - print_backtrace_dwarf(true /*demangle_flag*/); - std::terminate(); + //fprintf(stderr, "DArena::expand: reserve exhausted"); + //print_backtrace_dwarf(true /*demangle_flag*/); + //std::terminate(); return false; } diff --git a/xo-indentlog/include/xo/indentlog/scope.hpp b/xo-indentlog/include/xo/indentlog/scope.hpp index 42a664c9..8ed57497 100644 --- a/xo-indentlog/include/xo/indentlog/scope.hpp +++ b/xo-indentlog/include/xo/indentlog/scope.hpp @@ -125,6 +125,20 @@ namespace xo { public: template basic_scope(scope_setup setup, Tn&&... rest); + basic_scope(basic_scope && other) noexcept + : dest_sbuf_{other.dest_sbuf_}, + style_{other.style_}, + name1_{other.name1_}, + name2_{other.name2_}, + file_{other.file_}, + line_{other.line_}, + finalized_{other.finalized_} + { + other.finalized_ = true; + } + basic_scope(const basic_scope &) = delete; + basic_scope & operator=(const basic_scope &) = delete; + basic_scope & operator=(basic_scope &&) = delete; ~basic_scope(); bool enabled() const { return !finalized_; }