diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 349fa51..a6eb86a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -47,6 +47,7 @@ namespace xo { Generation gc_upto() const { return gc_upto_; } + bool is_idle() const { return mode_ == Mode::idle; } bool is_running() const { return mode_ == Mode::gc; } bool is_verify() const { return mode_ == Mode::verify; } @@ -62,7 +63,7 @@ namespace xo { struct GCStatistics { public: GCStatistics() = default; - explicit GCStatistics(uint32_t n_gc) : n_gc_{n_gc} {}; + //explicit GCStatistics(uint32_t n_gc) : n_gc_{n_gc} {}; uint32_t n_gc() const noexcept { return n_gc_; } diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index f816e05..e13773a 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -195,6 +195,9 @@ namespace xo { void cleanup_phase(Generation upto, bool sanitize_flag); + /** Revert to empty state **/ + void clear(); + private: /** configure @ref object_types_, using @p page_z **/ diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index cc38955..a62d522 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -84,6 +84,9 @@ namespace xo { void forward_mutation_log(obj gc, Generation upto); + /** Reset mutation log store to empty state **/ + void clear(); + private: /** aux init function: create mutation log **/ MutationLog _make_mlog(uint32_t igen, char tag_char, diff --git a/include/xo/gc/ObjectTypeSlot.hpp b/include/xo/gc/ObjectTypeSlot.hpp index 214d31c..c2b7162 100644 --- a/include/xo/gc/ObjectTypeSlot.hpp +++ b/include/xo/gc/ObjectTypeSlot.hpp @@ -20,9 +20,7 @@ namespace xo { **/ struct ObjectTypeSlot { ObjectTypeSlot() {} - explicit ObjectTypeSlot(AGCObject * iface) { - this->store_iface(iface); - } + //explicit ObjectTypeSlot(AGCObject * iface) { this->store_iface(iface); } /** true iff this slot is empty **/ bool is_null() const noexcept { diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 3eec1a7..643187e 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -322,7 +322,7 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { - return gco_store_.report_object_types(mm, error_mm, p_output); + return gco_store_.report_object_ages(mm, error_mm, p_output); } size_type @@ -672,15 +672,9 @@ namespace xo { void DX1Collector::clear() noexcept { - for (Role ri : Role::all()) { - for (Generation gj{0}; gj < config_.n_generation_; ++gj) { - DArena * arena = this->get_space(ri, gj); - - assert(arena); - - arena->clear(); - } - } + mlog_store_.clear(); + gco_store_.clear(); + root_set_.clear(); } void diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index c8eac5d..b6f0b6e 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -1182,6 +1182,21 @@ namespace xo { } while (fixup_work > 0); } /*_forward_children_until_fixpoint*/ + void + GCObjectStore::clear() + { + object_types_.clear(); + + for (Role ri : Role::all()) { + for (Generation gj{0}; gj < config_.n_generation_; ++gj) { + DArena * arena = this->get_space(ri, gj); + + assert(arena); + + arena->clear(); + } + } + } } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 0719423..7c1a8d7 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -560,6 +560,20 @@ namespace xo { return counters; } + void + MutationLogStore::clear() + { + // parallels .init_mlogs(), see also + + for (uint32_t igen = 0, ngen = config_.n_generation_; igen + 1 < ngen; ++igen) { + if (igen + 1 < c_max_generation) { + for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) { + this->mlog_storage_[mlog_role][igen].clear(); + } + } + } + } + } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index f226762..dee2b67 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -11,6 +11,7 @@ set(UTEST_SRCS GCObjectStore.test.cpp GCObjectConversion.test.cpp Object2.test.cpp + ObjectAge.test.cpp DMockCollector.cpp ICollector_DMockCollector.cpp diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 53d11ce..a9df7eb 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -25,6 +25,7 @@ namespace xo { using xo::mm::DArena; using xo::mm::DArenaIterator; using xo::mm::X1CollectorConfig; + using xo::mm::Generation; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; using xo::mm::cmpresult; @@ -34,11 +35,16 @@ namespace xo { using std::byte; namespace ut { + TEST_CASE("DX1CollectorIterator-0", "[alloc2][gc][DX1Collector]") + { + } + TEST_CASE("IAllocIterator_Xfer_DX1CollectorIterator", "[alloc2]") { /* verify IAllocIterator_Xfer is constructible + satisfies concept checks */ IAllocIterator_Xfer xfer; REQUIRE(IAllocIterator_Xfer::_valid); + } TEST_CASE("DX1CollectorIterator-1", "[alloc2][gc][DX1Collector]") @@ -128,10 +134,25 @@ namespace xo { auto ix = gc.begin(); auto end_ix = gc.end(); + REQUIRE(ix != DX1CollectorIterator::invalid()); + REQUIRE(end_ix != DX1CollectorIterator::invalid()); + REQUIRE(ix.is_valid()); REQUIRE(end_ix.is_valid()); REQUIRE(ix != end_ix); + REQUIRE(ix.gen_ix() == Generation::g0()); + REQUIRE(ix.gen_ix() < gc.config_.n_generation_); + REQUIRE(ix.gen_hi() == gc.config_.n_generation_); + REQUIRE(ix.arena_ix() == gc.to_space(Generation::g0())->begin()); + REQUIRE(ix.arena_hi() == gc.to_space(Generation::g0())->end()); + + { + // we have one alloc, so can visit it + auto info = *ix; + REQUIRE(info.is_valid()); + } + /* verify obj 'fat pointer' packaging */ auto ix_vt = with_facet::mkobj(&ix); auto end_ix_vt = with_facet::mkobj(&end_ix); diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index f37ea67..956282d 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -24,9 +24,13 @@ namespace ut { using xo::scm::DInteger; using xo::mm::MutationLogStore; using xo::mm::MutationLogConfig; + using xo::mm::MutationLog; + using xo::mm::MutationLogEntry; using xo::mm::GCObjectStore; using xo::mm::GCObjectStoreConfig; using xo::mm::DGCObjectStoreVisitor; + using xo::mm::Role; + using xo::mm::Generation; using xo::mm::DArena; using xo::mm::ArenaConfig; using xo::mm::X1VerifyStats; @@ -434,6 +438,24 @@ namespace ut { // fixture.mls_.init_mlogs(getpagesize()); + { + MutationLog * to_0_mlog + = fixture.mls_.get_mlog(Role::to_space(), + Generation::g0()); + MutationLog * from_0_mlog + = fixture.mls_.get_mlog(Role::from_space(), + Generation::g0()); + MutationLog * triage_0_mlog + = fixture.mls_.triage_mlog(Generation::g0()); + + REQUIRE(to_0_mlog); + REQUIRE(from_0_mlog); + REQUIRE(triage_0_mlog); + REQUIRE(to_0_mlog != from_0_mlog); + REQUIRE(to_0_mlog != triage_0_mlog); + REQUIRE(from_0_mlog != triage_0_mlog); + } + { // updates counters in fixture.verify_stats_ fixture.gcos_.verify_ok(); @@ -454,6 +476,13 @@ namespace ut { GCObjectStore & gcos = fixture.gcos_; MutationLogStore & mls = fixture.mls_; + { + MutationLogEntry mentry; + + REQUIRE(mentry.parent() == nullptr); + REQUIRE(mentry.p_data() == nullptr); + } + { // gcos setup. parallels GCObjectStore.test.cpp { diff --git a/utest/ObjectAge.test.cpp b/utest/ObjectAge.test.cpp new file mode 100644 index 0000000..a977212 --- /dev/null +++ b/utest/ObjectAge.test.cpp @@ -0,0 +1,34 @@ +/** @file ObjectAge.test.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "object_age.hpp" +#include + +namespace xo { + using xo::mm::object_age; + + namespace ut { + + TEST_CASE("ObjectAge-1", "[ObjectAge]") + { + REQUIRE(object_age{0} != object_age{1}); + REQUIRE(object_age{0} < object_age{1}); + REQUIRE(object_age{1} > object_age{0}); + + { + bool x = (object_age{0} > object_age{1}); + REQUIRE(x == false); + } + + { + bool x = (object_age{1} < object_age{0}); + REQUIRE(x == false); + } + } + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end ObjectAge.test.cpp */ diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 00999be..481d119 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -15,6 +15,7 @@ #include #include +#include //#include #include @@ -43,9 +44,14 @@ namespace ut { using xo::mm::AGCObject; using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; + using xo::mm::GCRoot; + using xo::mm::GCObjectStore; + using xo::mm::GCStatistics; using xo::mm::DArena; using xo::mm::ArenaConfig; using xo::mm::Generation; + using xo::mm::c_n_role; + using xo::mm::object_age; using xo::mm::Role; using xo::mm::padding; using xo::facet::obj; @@ -59,17 +65,14 @@ namespace ut { namespace { struct testcase_x1 { - testcase_x1(std::size_t nz, - std::size_t tz, + testcase_x1(std::size_t gz, std::size_t n_gct, std::size_t t_gct) - : nursery_z_{nz}, - tenured_z_{tz}, + : generation_z_{gz}, incr_gc_threshold_{n_gct}, full_gc_threshold_{t_gct} {} - std::size_t nursery_z_; - std::size_t tenured_z_; + std::size_t generation_z_; std::size_t incr_gc_threshold_; std::size_t full_gc_threshold_; }; @@ -79,13 +82,50 @@ namespace ut { // n_gct: nursery gc threshold // t_gct: tenured gc threshold // - // nz tz n_gct t_gct - testcase_x1(4096, 8192, 1024, 1024) + // gz n_gct t_gct + testcase_x1(8192, 1024, 1024) }; } static InitEvidence s_init = (InitSubsys::require()); + TEST_CASE("x1-config", "[gc][x1]") + { + // real purpose: ensure s_init survives static linking + REQUIRE(s_init.evidence()); + + Subsystem::initialize_all(); + + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag), "x1-config test"); + + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + scope log(XO_DEBUG(false), xtag("i_tc", i_tc)); + + const testcase_x1 & tc = s_testcase_v[i_tc]; + + X1CollectorConfig cfg{ .name_ = "x1_test", + .arena_config_ = ArenaConfig{ + .size_ = tc.generation_z_, + .store_header_flag_ = true}, + .object_types_z_ = 16384, + .gc_trigger_v_{{ + tc.incr_gc_threshold_, + tc.full_gc_threshold_}}, + .debug_flag_ = c_debug_flag, + }; + + REQUIRE(cfg.n_generation_ == 2); + REQUIRE(cfg.n_survive_threshold_ == 2); + REQUIRE(cfg.age2gen(object_age{0}) == Generation::g0()); + REQUIRE(cfg.age2gen(object_age{1}) == Generation::g0()); + REQUIRE(cfg.age2gen(object_age{2}) == Generation::g1()); + REQUIRE(cfg.age2gen(object_age{99}) == Generation::g1()); + + REQUIRE(cfg.promotion_threshold(Generation::g1()) == cfg.n_survive_threshold_); + } + } + TEST_CASE("x1", "[gc][x1]") { // real purpose: ensure s_init survives static linking @@ -108,17 +148,44 @@ namespace ut { X1CollectorConfig cfg{ .name_ = "x1_test", .arena_config_ = ArenaConfig{ - .size_ = tc.tenured_z_, + .size_ = tc.generation_z_, .store_header_flag_ = true}, .object_types_z_ = 16384, .gc_trigger_v_{{ tc.incr_gc_threshold_, tc.full_gc_threshold_}}, - .debug_flag_ = c_debug_flag, - }; + .sanitize_flag_ = true, + .debug_flag_ = c_debug_flag }; DX1Collector gc(cfg); + { + // X1Collector never uses the null ctor here. + // but it relies on DArenaVector, + // which requires it for DArenaVector::resize() + // + GCRoot null_root; + + REQUIRE(null_root.root() == nullptr); + } + + // secondary allocator for reporting + DArena report_arena(ArenaConfig() + .with_name("x1_test_report_arena") + .with_size(64 * 1024)); + auto report_mm = obj(&report_arena); + + DArena error_arena(ArenaConfig() + .with_name("x1_test_error_arena") + .with_size(16 * 1024)); + auto error_mm = obj(&error_arena); + + const GCObjectStore * p_gco = nullptr; + { + const DX1Collector & c_gc = gc; + p_gco = &(c_gc.gco_store()); + } + CollectorTypeRegistry::instance() .install_types(obj(&gc)); @@ -126,7 +193,9 @@ namespace ut { /* verify configuration */ { - REQUIRE(cfg.n_generation_ == 2); + REQUIRE(gc.config().arena_config_.size_ == tc.generation_z_); + REQUIRE(gc.config().arena_config_.store_header_flag_ == true); + REQUIRE(gc.config().n_generation_ == 2); } /* verify initial collector state */ @@ -148,8 +217,8 @@ namespace ut { const DArena * from_0 = gc.get_space(Role::from_space(), Generation{0}); REQUIRE(from_0 != nullptr); - REQUIRE(from_0->reserved() >= tc.tenured_z_); - REQUIRE(from_0->reserved() < tc.tenured_z_ + from_0->page_z_); + REQUIRE(from_0->reserved() >= tc.generation_z_); + REQUIRE(from_0->reserved() < tc.generation_z_ + from_0->page_z_); REQUIRE(from_0->reserved() % from_0->page_z_ == 0); REQUIRE(from_0->allocated() == 0); @@ -203,18 +272,30 @@ namespace ut { ok = c_o.is_type_installed(typeseq::id()); REQUIRE(ok); + REQUIRE(gc_o.name() == cfg.name_); + // nothing committed yet (?) + REQUIRE(gc_o.size() == cfg.object_types_z_); + // no-op + REQUIRE(gc_o.expand(0)); + REQUIRE(gc_o.size() == cfg.object_types_z_); + + // x0_o will be added as gc root. x0_o_orig will not auto x0_o = DFloat::box(gc_o, 3.1415927); + auto x0_o_orig = x0_o; c_o.add_gc_root(&x0_o); REQUIRE(to_0->allocated() == sizeof(AllocHeader) + sizeof(DFloat)); + // n1_o will be added as gc root. n1_o_orig will not auto n1_o = DInteger::box(gc_o, 42); + auto n1_o_orig = n1_o; c_o.add_gc_root(&n1_o); + REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + sizeof(AllocHeader) + sizeof(DInteger))); - //DList * l0 = DList::list(gc_o, x0_o); - //auto l0_o = with_facet::mkobj(l0); + // l0_o will be added as gc root. l0_o_orig will not auto l0_o = ListOps::list(gc_o, x0_o); + auto l0_o_orig = l0_o; c_o.add_gc_root(&l0_o); REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + sizeof(AllocHeader) + sizeof(DInteger) @@ -231,11 +312,23 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)x0_o.data()); + auto float_tseq = typeseq::id(); + auto x0_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == float_tseq.seqno()); REQUIRE(info.size() >= sizeof(DFloat)); REQUIRE(info.size() < sizeof(DFloat) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DFloat) <= x0_alloc_z); + REQUIRE(x0_alloc_z <= sizeof(DFloat) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(float_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(float_tseq)); + REQUIRE(gc.lookup_type(float_tseq)->_typeseq() == float_tseq); } { @@ -245,11 +338,23 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)n1_o.data()); + auto integer_tseq = typeseq::id(); + auto n1_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == integer_tseq.seqno()); REQUIRE(info.size() >= sizeof(DInteger)); REQUIRE(info.size() < sizeof(DInteger) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DInteger) <= n1_alloc_z); + REQUIRE(n1_alloc_z <= sizeof(DInteger) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(integer_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(integer_tseq)); + REQUIRE(gc.lookup_type(integer_tseq)->_typeseq() == integer_tseq); } { @@ -259,14 +364,32 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)l0_o.data()); + auto list_tseq = typeseq::id(); + auto l0_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == list_tseq.seqno()); REQUIRE(info.size() >= sizeof(DList)); REQUIRE(info.size() < sizeof(DList) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DList) <= l0_alloc_z); + REQUIRE(l0_alloc_z <= sizeof(DList) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(list_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(list_tseq)); + REQUIRE(gc.lookup_type(list_tseq)->_typeseq() == list_tseq); } } + { + GCStatistics stats = gc.gc_stats(); + + REQUIRE(stats.n_gc() == 0); + } + /* no GC roots, so GC is trivial */ c_o.request_gc(Generation{1}); @@ -274,20 +397,54 @@ namespace ut { log && log(xtag("l0_o.data()->head_.data()", l0_o.data()->head_.data())); log && log(xtag("x0_o.data()", x0_o.data())); - REQUIRE(!gc.contains(Role::from_space(), x0_o.data())); - REQUIRE(gc.contains(Role::to_space(), x0_o.data())); - REQUIRE(x0_o.data()->value() == 3.1415927); + // gcobjectstore is stable + REQUIRE(&gc.gco_store() == p_gco); - REQUIRE(!gc.contains(Role::from_space(), n1_o.data())); - REQUIRE(gc.contains(Role::to_space(), n1_o.data())); - REQUIRE(n1_o.data()->value() == 42); + REQUIRE(gc.runstate().gc_upto() == Generation::sentinel()); + REQUIRE(gc.runstate().is_idle()); + REQUIRE(!gc.runstate().is_running()); + REQUIRE(!gc.runstate().is_verify()); - REQUIRE(!gc.contains(Role::from_space(), l0_o.data())); - REQUIRE(gc.contains(Role::to_space(), l0_o.data())); - REQUIRE(l0_o.data()->is_empty() == false); + { + REQUIRE(!gc.contains(Role::from_space(), x0_o.data())); + REQUIRE(gc.contains(Role::to_space(), x0_o.data())); + REQUIRE(x0_o.data()->value() == 3.1415927); - REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); - REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + // former location of x0_o now in from-space + REQUIRE(gc.contains(Role::from_space(), x0_o_orig.data())); + REQUIRE(gc.locate_address(x0_o_orig.data()) == -1); + } + + { + REQUIRE(!gc.contains(Role::from_space(), n1_o.data())); + REQUIRE(gc.contains(Role::to_space(), n1_o.data())); + REQUIRE(n1_o.data()->value() == 42); + + REQUIRE(gc.contains(Role::from_space(), n1_o_orig.data())); + REQUIRE(gc.locate_address(n1_o_orig.data()) == -1); + } + + { + REQUIRE(!gc.contains(Role::from_space(), l0_o.data())); + REQUIRE(gc.contains(Role::to_space(), l0_o.data())); + REQUIRE(l0_o.data()->is_empty() == false); + + REQUIRE(gc.contains(Role::from_space(), l0_o_orig.data())); + REQUIRE(gc.locate_address(l0_o_orig.data()) == -1); + + REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); + REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + } + + { + // verify a non-gc-owned address + int x = 999; + + REQUIRE(-1 == gc.locate_address(&x)); + } + + REQUIRE(gc.size_total() == gc.committed()); + REQUIRE(gc.mutation_log_entries() == 0); Generation g0 = Generation::g0(); REQUIRE(c_o.committed(g0, Role::to_space()) == to0_commit_z); @@ -295,6 +452,50 @@ namespace ut { REQUIRE(c_o.locate_address(x0_o.data()) >= 0); REQUIRE(c_o.contains(Role::to_space(), x0_o.data())); + GCStatistics stats = gc.gc_stats(); + REQUIRE(stats.n_gc() == 1); + + { + obj report; + bool ok = c_o.report_statistics(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + { + obj report; + bool ok = c_o.report_object_ages(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + { + obj report; + bool ok = c_o.report_object_types(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + gc_o.clear(); + { + REQUIRE(gc_o.allocated() == 0); + } + } catch (std::exception & ex) { std::cerr << "caught exception: " << ex.what() << std::endl; REQUIRE(false);