diff --git a/README.md b/README.md index daefbe06..25660760 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,19 @@ $ nix-build -A xo-userenv-slow Same result as `$nix-build -A xo-userenv`, but builds each package serially using `xo-build`. +### Coverage Build + +Prepare build +``` +# phase 2 +$ cmake -B .build -S . -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=Debug -DCODE_COVERAGE=ON +``` + +Build coverage-enabled libraries and executables +``` +$ (cd .build && make ccov) +``` + ## To view docs from WSL 1. find wsl IP address diff --git a/docs/install.rst b/docs/install.rst index 611e34d9..45d5b66f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -92,6 +92,11 @@ Aternatively can enter nix environment, then follow instructions for cmake build # etc +coverage build +-------------- + +See ``Test Coverage Setup`` under ``Development`` below + Development =========== @@ -109,6 +114,30 @@ To setup xo-umbrella2 build to work with a language server: In this case subsystem LSP setup should be omitted, git root is ``path/to/xo-umbrella2``, not ``path/to/xo-umbrella2/xo-ratio`` etc. +Test Coverage Setup +------------------- + +To setup a unit test coverage build/ccov/all-merged + +.. code-block:: + + # can reuse phase 1 cmake-macros-install + + # phase 2 + $ cmake -B .build -S . -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_BUILD_TYPE=coverage + +Then run unit tests + + $ (cd .build && ctest) + +To build coverage report + + $ (.build/gen-ccov) + +Html report in ``.build/ccov/html/index.html`` + + + Sphinx Autobuild Setup ---------------------- diff --git a/xo-alloc/include/xo/alloc/GC.hpp b/xo-alloc/include/xo/alloc/GC.hpp index 0c50a722..6f9573f3 100644 --- a/xo-alloc/include/xo/alloc/GC.hpp +++ b/xo-alloc/include/xo/alloc/GC.hpp @@ -218,6 +218,7 @@ namespace xo { public: /** create new GC instance with configuration @p config **/ explicit GC(const Config & config); + virtual ~GC(); /** create GC allocator. * diff --git a/xo-alloc/src/alloc/ArenaAlloc.cpp b/xo-alloc/src/alloc/ArenaAlloc.cpp index 227e2d63..c3f70d8c 100644 --- a/xo-alloc/src/alloc/ArenaAlloc.cpp +++ b/xo-alloc/src/alloc/ArenaAlloc.cpp @@ -56,6 +56,8 @@ namespace xo { if (lo_ <= x && x < limit_) { this->free_ptr_ = x; + if (this->checkpoint_ > free_ptr_) + this->checkpoint_ = free_ptr_; } else { throw std::runtime_error(tostr("LinearAllog::set_free_ptr(x): expected lo <= x < limit", xtag("lo", lo_), xtag("x", x), xtag("limit", limit_))); @@ -102,8 +104,7 @@ namespace xo { void ArenaAlloc::clear() { - this->checkpoint_ = lo_; - this->free_ptr_ = lo_; + this->set_free_ptr(lo_); this->limit_ = hi_ - redline_z_; } @@ -133,14 +134,14 @@ namespace xo { std::byte * retval = this->free_ptr_; - this->free_ptr_ += z1; - log && log(xtag("self", name_), xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1)); - if (free_ptr_ > limit_) { + if (free_ptr_ + z1 > limit_) { return nullptr; } + this->free_ptr_ += z1; + return retval; } diff --git a/xo-alloc/src/alloc/Forwarding1.cpp b/xo-alloc/src/alloc/Forwarding1.cpp index ca4f051b..32d95b60 100644 --- a/xo-alloc/src/alloc/Forwarding1.cpp +++ b/xo-alloc/src/alloc/Forwarding1.cpp @@ -26,23 +26,29 @@ namespace xo { return dest_.ptr(); } + // LCOV_EXCL_START std::size_t Forwarding1::_shallow_size() const { assert(false); return 0; } + // LCOV_EXCL_STOP + // LCOV_EXCL_START Object * Forwarding1::_shallow_copy() const { assert(false); return nullptr; } + // LCOV_EXCL_STOP + // LCOV_EXCL_START std::size_t Forwarding1::_forward_children() { assert(false); return 0; } + // LCOV_EXCL_STOP } /*namespace obj*/ } /*namespace xo*/ diff --git a/xo-alloc/src/alloc/GC.cpp b/xo-alloc/src/alloc/GC.cpp index 6dd8a929..4f106779 100644 --- a/xo-alloc/src/alloc/GC.cpp +++ b/xo-alloc/src/alloc/GC.cpp @@ -141,6 +141,21 @@ namespace xo { this->checkpoint(); } + GC::~GC() { + /* hygiene */ + this->clear(); + + nursery_[role2int(role::from_space)].reset(); + nursery_[role2int(role::to_space) ].reset(); + + tenured_[role2int(role::from_space)].reset(); + tenured_[role2int(role::to_space) ].reset(); + + mutation_log_[role2int(role::from_space)].reset(); + mutation_log_[role2int(role::to_space) ].reset(); + defer_mutation_log_.reset(); + } + up GC::make(const Config & config) { @@ -245,8 +260,10 @@ namespace xo { return nursery_[role2int(role::to_space)]->free_ptr(); case generation::tenured: return tenured_[role2int(role::to_space)]->free_ptr(); + // LCOV_EXCL_START case generation::N: assert(false); + // LCOV_EXCL_STOP } return nullptr; @@ -647,7 +664,7 @@ namespace xo { Object * parent_to = from_entry.parent_destination(); - log(xtag("parent_to", (void*)parent_to)); + log && log(xtag("parent_to", (void*)parent_to)); assert(tospace_generation_of(parent_to) == generation_result::tenured); diff --git a/xo-alloc/src/alloc/IAlloc.cpp b/xo-alloc/src/alloc/IAlloc.cpp index 6e06a644..8fe4789a 100644 --- a/xo-alloc/src/alloc/IAlloc.cpp +++ b/xo-alloc/src/alloc/IAlloc.cpp @@ -47,12 +47,14 @@ namespace xo { *lhs = rhs; } + // LCOV_EXCL_START std::byte * IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/) { assert(false); return nullptr; } + // LCOV_EXCL_STOP } /*namespace gc*/ } /*namespace xo*/ diff --git a/xo-alloc/utest/CMakeLists.txt b/xo-alloc/utest/CMakeLists.txt index 50882bba..b38a442f 100644 --- a/xo-alloc/utest/CMakeLists.txt +++ b/xo-alloc/utest/CMakeLists.txt @@ -5,6 +5,7 @@ set(UTEST_EXE utest.alloc) set(UTEST_SRCS alloc_utest_main.cpp + IAlloc.test.cpp ArenaAlloc.test.cpp GC.test.cpp) diff --git a/xo-object/include/xo/object/List.hpp b/xo-object/include/xo/object/List.hpp index 9c8cd31a..caf3be5a 100644 --- a/xo-object/include/xo/object/List.hpp +++ b/xo-object/include/xo/object/List.hpp @@ -15,6 +15,9 @@ namespace xo { /** the empty list. unique sentinel object **/ static gp nil; + /** @return non-null iff @p x is actually a List cell (or nil) **/ + static gp from(gp x); + /** @return list with first element @p car, and tail @p cdr **/ static gp cons(gp car, gp cdr); diff --git a/xo-object/include/xo/object/String.hpp b/xo-object/include/xo/object/String.hpp index a96cdd20..a2719318 100644 --- a/xo-object/include/xo/object/String.hpp +++ b/xo-object/include/xo/object/String.hpp @@ -10,11 +10,13 @@ namespace xo { namespace obj { class String : public Object { public: - enum Owner { unique, shared }; + enum class owner { unique, shared }; /** donwcast from @p x iff x is actually a String. Otherwise nullptr **/ static gp from(gp x); + /** create shared string @p s, using allocator @ref Object::mm **/ + static gp share(const char * s); /** create copy of string @p s, using allocator @ref Object::mm **/ static gp copy(const char * s); /** create copy of string @p s, using allocator @p mm **/ @@ -35,14 +37,14 @@ namespace xo { virtual std::size_t _forward_children() override; private: - String(Owner owner, std::size_t z, char * s); + String(owner owner, std::size_t z, char * s); /** create instance, copying string contents (when @p copy_flag is true) using allocator @p mm **/ - String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy); + String(gc::IAlloc * mm, owner owner, std::size_t z, char * s); private: /** true iff storage in @ref chars_ is owned by this String. **/ - Owner owner_ = Owner::shared; + owner owner_ = owner::shared; /** length of @ref chars_ in bytes (storage allocated, not necessarily string length) **/ std::size_t z_chars_ = 0; /** string contents. always null-terminated **/ diff --git a/xo-object/src/object/Boolean.cpp b/xo-object/src/object/Boolean.cpp index c91fb2c6..ad5bbeb7 100644 --- a/xo-object/src/object/Boolean.cpp +++ b/xo-object/src/object/Boolean.cpp @@ -19,15 +19,29 @@ namespace xo { return s_boolean_v[static_cast(x)]; } + gp + Boolean::true_obj() + { + return boolean_obj(true); + } + + gp + Boolean::false_obj() + { + return boolean_obj(false); + } + std::size_t Boolean::_shallow_size() const { return sizeof(Boolean); } + // LCOV_EXCL_START Object * Boolean::_shallow_copy() const { + /* Boolean instances not created in GC-owned space, * so GC will not traverse them. * @@ -38,14 +52,18 @@ namespace xo { assert(false); return nullptr; - } + } + // LCOV_EXCL_STOP + + // LCOV_EXCL_START std::size_t Boolean::_forward_children() { assert(false); return 0; } + // LCOV_EXCL_STOP } } /*namespace xo*/ diff --git a/xo-object/src/object/List.cpp b/xo-object/src/object/List.cpp index 446aa919..00e9c19c 100644 --- a/xo-object/src/object/List.cpp +++ b/xo-object/src/object/List.cpp @@ -16,6 +16,11 @@ namespace xo { gp List::nil = new List(nullptr, nullptr); + gp + List::from(gp x) { + return dynamic_cast(x.ptr()); + } + gp List::cons(gp car, gp cdr) { return new (MMPtr(mm)) List(car, cdr); diff --git a/xo-object/src/object/String.cpp b/xo-object/src/object/String.cpp index f1e097bd..e2767673 100644 --- a/xo-object/src/object/String.cpp +++ b/xo-object/src/object/String.cpp @@ -12,14 +12,14 @@ namespace xo { namespace obj { - String::String(Owner owner, std::size_t z, char * s) + String::String(owner owner, std::size_t z, char * s) : owner_{owner}, z_chars_{z}, chars_{s} {} - String::String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy) + String::String(gc::IAlloc * mm, owner owner, std::size_t z, char * s) : owner_{owner}, z_chars_{z} { - if (copy) { + if (owner_ == owner::unique) { chars_ = reinterpret_cast(mm->alloc(z)); assert(chars_); @@ -35,6 +35,14 @@ namespace xo { return dynamic_cast(x.ptr()); } + gp + String::share(const char * s) { + const char * chars = s ? s : ""; + std::size_t z = 1 + ::strlen(chars); + + return new (MMPtr(mm)) String(mm, owner::shared, z, const_cast(chars)); + } + gp String::copy(const char * s) { return copy(Object::mm, s); @@ -47,13 +55,13 @@ namespace xo { const char * chars = s ? s : ""; // const-cast ok since chars copied with Owner::unique - return new (MMPtr(mm)) String(mm, Owner::unique, z, const_cast(chars), true /*copy*/); + return new (MMPtr(mm)) String(mm, owner::unique, z, const_cast(chars)); } gp String::allocate(std::size_t z) { - return new (MMPtr(Object::mm)) String(mm, Owner::unique, z, const_cast(""), true /*copy*/); + return new (MMPtr(Object::mm)) String(mm, owner::unique, z, const_cast("")); } gp @@ -63,10 +71,11 @@ namespace xo { std::size_t z2 = s2->length(); std::size_t z = z1 + z2; - gp retval = allocate(z); + // +1 for null terminator + gp retval = allocate(z+1); - strlcpy(retval->chars_, s1->chars_, z1); - strlcpy(retval->chars_ + z1, s2->chars_, z2); + strlcpy(retval->chars_, s1->chars_, z1+1); + strlcpy(retval->chars_ + z1, s2->chars_, z2+1); return retval; } @@ -87,7 +96,7 @@ namespace xo { */ std::size_t retval = gc::IAlloc::with_padding(sizeof(String)); - if (owner_ == Owner::unique) + if (owner_ == owner::unique) retval += gc::IAlloc::with_padding(z_chars_); return retval; @@ -109,7 +118,7 @@ namespace xo { // gp copy = new (cpof) String(owner_, z_chars_, chars_); - if (owner_ == Owner::unique) { + if (owner_ == owner::unique) { std::byte * mem = reinterpret_cast(chars_); copy->chars_ = reinterpret_cast(Object::mm->alloc_gc_copy(z_chars_, mem)); diff --git a/xo-object/utest/CMakeLists.txt b/xo-object/utest/CMakeLists.txt index 90f33f9b..42ed1c57 100644 --- a/xo-object/utest/CMakeLists.txt +++ b/xo-object/utest/CMakeLists.txt @@ -1,12 +1,14 @@ # build unittest object/utest -set(SELF_EXE utest.object) -set(SELF_SRCS +set(UTEST_EXE utest.object) +set(UTEST_SRCS object_utest_main.cpp + Boolean.test.cpp String.test.cpp List.test.cpp GC.test.cpp) -xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) -xo_self_dependency(${SELF_EXE} xo_object) -xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2) +xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) +xo_self_dependency(${UTEST_EXE} xo_object) +xo_dependency(${UTEST_EXE} randomgen) +xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) diff --git a/xo-object/utest/GC.test.cpp b/xo-object/utest/GC.test.cpp index 544dd149..83f4d60c 100644 --- a/xo-object/utest/GC.test.cpp +++ b/xo-object/utest/GC.test.cpp @@ -6,7 +6,12 @@ #include "xo/alloc/GC.hpp" #include "xo/object/List.hpp" #include "xo/object/Integer.hpp" +#include "xo/randomgen/random_seed.hpp" +#include "xo/randomgen/xoshiro256.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" #include +#include namespace xo { using xo::obj::List; @@ -15,6 +20,9 @@ namespace xo { using xo::gc::generation_result; using xo::gc::generation; + using xo::rng::Seed; + using xo::rng::xoshiro256ss; + namespace ut { // Also see GC unit tests in xo-alloc/utest @@ -36,7 +44,7 @@ namespace xo { { .initial_nursery_z_ = 1024, .initial_tenured_z_ = 2048, - .debug_flag_ = true + .debug_flag_ = false }); REQUIRE(gc->gc_statistics().n_mutation_ == 0); @@ -49,15 +57,24 @@ namespace xo { gp l = List::list(Integer::make(1)); gc->add_gc_root(reinterpret_cast(l.ptr_address())); + + gp l2 = List::list(Integer::make(10)); + gc->add_gc_root(reinterpret_cast(l2.ptr_address())); + { REQUIRE(l->size() == 1); + REQUIRE(l2->size() == 1); REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery); REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); - REQUIRE(gc->is_before_checkpoint(l.ptr()) == false); REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false); + REQUIRE(gc->tospace_generation_of(l2.ptr()) == generation_result::nursery); + REQUIRE(gc->tospace_generation_of(l2->head().ptr()) == generation_result::nursery); + REQUIRE(gc->is_before_checkpoint(l2.ptr()) == false); + REQUIRE(gc->is_before_checkpoint(l2->head().ptr()) == false); + REQUIRE(gc->mlog_size() == 0); } @@ -138,6 +155,358 @@ namespace xo { REQUIRE(gc->mlog_size() == 0); } } + + namespace { + enum class object_type { + nil, + integer, + cons, + }; + + struct ObjectModel { + /* 1:1 with address */ + std::size_t index_; + /* nil|integer|cons */ + object_type type_; + /* value for model of Integer::value_, if type_ is object_type::integer */ + std::size_t int_value_; + /* index# for model of List::head_, if type_ is object_type::list */ + std::size_t head_ix_; + /* index# for model of List::rest_, if type_ is object_type::list */ + std::size_t rest_ix_; + }; + + struct ObjectGraphModel { + /** + * @param from_graph + * @param from_ix + * @param to_graph + * @param to_ix + * @param p_visited_set + **/ + static bool verify_equal_aux(ObjectGraphModel & from_graph, + std::size_t from_ix, + ObjectGraphModel & to_graph, + std::size_t to_ix, + std::unordered_set * p_visited_set); + + /** compare models for structural equivalence; will be comparing before/after a garbage collection cycle + * @param from_graph model before GC + * @param to_graph model after GC + * @return true iff models are equivalent + **/ + static bool verify_equal_models(ObjectGraphModel & from_model, ObjectGraphModel & to_model); + + /* build model for object graph from a vector of object pointers */ + void from_root_vector(const std::vector> & object_v); + /* include everything reachable from @p x in this object model */ + std::size_t traverse_from_object(gp x); + + /* one node per xo::Object instance. */ + std::vector nodes_; + /* map from root index to node index number */ + std::vector roots_; + /* map from (original) address to index number */ + std::unordered_map addr2node_map_; + }; + + bool + ObjectGraphModel::verify_equal_aux(ObjectGraphModel & from_graph, + std::size_t from_ix, + ObjectGraphModel & to_graph, + std::size_t to_ix, + std::unordered_set * p_visited_set) + { + if (p_visited_set->contains(from_ix)) + return true; + + const ObjectModel & from = from_graph.nodes_.at(from_ix); + const ObjectModel & to = from_graph.nodes_.at(to_ix); + + REQUIRE(from.type_ == to.type_); + + p_visited_set->insert(from.index_); + + if (from.type_ == object_type::nil) + return true; + + if (from.type_ == object_type::integer) { + REQUIRE(from.int_value_ == to.int_value_); + return true; + } + + if (from.type_ == object_type::cons) { + return (verify_equal_aux(from_graph, from.head_ix_, + to_graph, to.head_ix_, + p_visited_set) + && verify_equal_aux(from_graph, from.rest_ix_, + to_graph, to.rest_ix_, + p_visited_set)); + } + + return false; + } + + bool + ObjectGraphModel::verify_equal_models(ObjectGraphModel & from_model, + ObjectGraphModel & to_model) + { + REQUIRE(from_model.roots_.size() == to_model.nodes_.size()); + REQUIRE(from_model.nodes_.size() == to_model.nodes_.size()); + + std::unordered_set visited_set; + + for (std::size_t i = 0, n = from_model.roots_.size(); i < n; ++i) { + INFO(tostr(xtag("i", i), xtag("n", n))); + + REQUIRE(verify_equal_aux(from_model, + from_model.roots_.at(i), + to_model, + to_model.roots_.at(i), + &visited_set)); + } + + return true; + } + + std::size_t + ObjectGraphModel::traverse_from_object(gp x) + { + std::uintptr_t x_addr = reinterpret_cast(x.ptr()); + + auto addr2node_ix = addr2node_map_.find(x_addr); + + if (addr2node_ix != addr2node_map_.end()) { + /* already imported (or import on call stack) */ + + return addr2node_ix->second; + } else { + ObjectModel new_model; + auto x_int = Integer::from(x); + auto x_list = List::from(x); + + std::size_t new_index = this->nodes_.size(); + { + if (x_int.is_null() && x_list.is_null()) + throw std::runtime_error("expecting object graph containing int|cons only"); + + if (!x_int.is_null()) { + new_model.index_ = new_index; + new_model.type_ = object_type::integer; + new_model.int_value_ = x_int->value(); + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } + + if (!x_list.is_null()) { + + if (x_list->is_nil()) { + new_model.index_ = 0; + new_model.type_ = object_type::nil; + new_model.int_value_ = 0; + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } else { + new_model.index_ = new_index; + new_model.type_ = object_type::cons; + new_model.int_value_ = 0; + /* fill below */ + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } + } + } + + this->nodes_.push_back(new_model); + this->addr2node_map_[x_addr] = new_index; + + if (!x_list.is_null() && !(x_list->is_nil())) { + ObjectModel & model = this->nodes_.at(new_index); + + model.head_ix_ = traverse_from_object(x_list->head()); + model.rest_ix_ = traverse_from_object(x_list->rest()); + } + + return new_index; + } + } + + void + ObjectGraphModel::from_root_vector(const std::vector> & root_v) + { + assert(nodes_.empty()); + assert(addr2node_map_.empty()); + + /* sentinel = List::nil */ + { + ObjectModel sentinel; + sentinel.index_ = 0; + sentinel.type_ = object_type::nil; + sentinel.int_value_ = 0; + sentinel.head_ix_ = 0; + sentinel.rest_ix_ = 0; + + this->nodes_.push_back(sentinel); + } + + /* it's possible that object_v is complete. + * seed model by importing all the nodes in object_v[] + */ + for (gp x : root_v) + this->roots_.push_back(traverse_from_object(x)); + } + + struct testcase_stresstest { + testcase_stresstest(std::size_t nz, std::size_t tz, std::size_t m, std::size_t n, std::size_t r, std::size_t k, bool debug_flag) + : nursery_z_{nz}, tenured_z_{tz}, m_{m}, n_{n}, r_{r}, k_{k}, debug_flag_{debug_flag} + {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + + /* #of random list cells to create */ + std::size_t m_; + /* #of random integers to create */ + std::size_t n_; + /* #of gc roots to create */ + std::size_t r_; + /* #of random mutations */ + std::size_t k_; + + bool debug_flag_; + }; + + std::vector s_testcase_v = + { + testcase_stresstest(1024, 1024, 3, 7, 5, 10, true) + }; + } /*namespace*/ + + TEST_CASE("gc-stresstest", "[alloc][gc][gc_mutation]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const testcase_stresstest & tc = s_testcase_v[i_tc]; + + scope log(XO_DEBUG(tc.debug_flag_)); + + up gc = GC::make( + { + .initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .debug_flag_ = tc.debug_flag_ + }); + + REQUIRE(gc->gc_statistics().n_mutation_ == 0); + REQUIRE(gc->gc_statistics().n_logged_mutation_ == 0); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + // Plan: + // - create vector of m cons cells w1[]. + // - prepend w1[] to a vector of n integers; call this w2[]. + // - create vector root_v[] of r gc roots. Assign each root_v[j] to some random w2[i] + // - make some random mutations. + // - traverse root_v[] to construct model from_model for reachable objects + // - run gc + // - traverse root_v[] again, to construct to_model for eachable objects + // - verify from_model ~=~ to_model + + uint64_t seed = 8365237040761243362UL; + //Seed seed; // to seed from /dev/random + //std::cerr << "seed=" << seed << std::endl; + auto rgen = xoshiro256ss(seed); + + /* create m random list cells */ + size_t m = tc.m_; + /* create n random integers */ + size_t n = tc.n_; + /* #of roots */ + size_t r = tc.r_; + /* #of random mutations */ + size_t k = tc.k_; + + REQUIRE(m > 0); + REQUIRE(n > 0); + + /* w1[] contains some random list cells */ + std::vector> w1; + { + for (size_t i = 0; i < m; ++i) { + w1.push_back(List::cons(List::nil, List::nil)); + } + REQUIRE(w1.size() == m); + } + + /* w2[] has all of w1[], also contains some integers */ + std::vector> w2; + { + std::copy(w1.begin(), w1.end(), std::back_inserter(w2)); + for (size_t j = 0; j < n; ++j) { + w2.push_back(Integer::make(j)); + } + REQUIRE(w2.size() == m + n); + } + + /* create some random roots. always pick at least one list cell */ + std::vector> root_v; + std::size_t w1_ix = rgen() % n; + { + root_v.push_back(w2.at(w1_ix)); + for (std::size_t i = 1; i < r; ++i) { + std::size_t w2_ix = rgen() % (m + n); + + root_v.push_back(w2.at(w2_ix)); + } + + for (auto & root : root_v) + gc->add_gc_root(root.ptr_address()); + } + + /* random mutations -- these will get logged */ + { + for (std::size_t i = 0; i < k; ++i) { + /* pick a list cell at random */ + gp l1 = w1.at(rgen() % w1.size()); + + if (rgen() % 2 == 0) { + /* pick another list cell at random, and link it to l1 */ + gp l2 = w1.at(rgen() % w1.size()); + + l1->assign_rest(l2); + } else { + /* pick a value at random (could be list or integer), + * assign to head + */ + gp x2 = w2.at(rgen() % w2.size()); + + l1->assign_head(x2); + } + } + } + + log && log("stats.before", gc->gc_statistics()); + + /* make model for contents of w2[] */ + ObjectGraphModel from_model; + from_model.from_root_vector(root_v); + + gc->request_gc(generation::nursery); + + /* collector cycle changed object addresses. + * build a new object model, and verify that they're equivalent + */ + + ObjectGraphModel to_model; + to_model.from_root_vector(root_v); + + REQUIRE(ObjectGraphModel::verify_equal_models(from_model, to_model)); + + log && log("stats.after", gc->gc_statistics()); + } + } } /*namespace ut*/ } /*namespace xo*/ diff --git a/xo-object/utest/List.test.cpp b/xo-object/utest/List.test.cpp index 862b6ae5..d7812010 100644 --- a/xo-object/utest/List.test.cpp +++ b/xo-object/utest/List.test.cpp @@ -68,6 +68,8 @@ namespace xo { std::size_t expected_alloc_z = 0; + // TODO: consolidate: root setup shared with "List" unit test + /* construct example Lists from testcase info */ for (const std::vector & v : tc.v_) { @@ -179,6 +181,102 @@ namespace xo { } } /*TEST_CASE(List, ..)*/ + TEST_CASE("List-cyclic", "[List][gc][cycles]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_List & tc = s_testcase_v[i_tc]; + + constexpr bool c_debug_flag = false; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .debug_flag_ = c_debug_flag}); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + { + scope log(XO_DEBUG(c_debug_flag)); + log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size())); + + std::vector> root_v(tc.v_.size()); + std::size_t i = 0; + + std::size_t expected_alloc_z = 0; + + // TODO: consolidate: root setup shared with "List" unit test + + /* construct example Lists from testcase info */ + for (const std::vector & v : tc.v_) + { + /* building l1 in reverse order */ + gp l1 = List::nil; + gp last = List::nil; + + for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) { + const std::string & si = v.at(ip1 - 1); + log && log(xtag("i", ip1-1), xtag("si", si)); + gp sobj = String::copy(si.c_str()); + l1 = List::cons(sobj, l1); + log && log(xtag("l1.size", l1->size())); + if (ip1 == v.size()) { + // capture last + last = l1; + } + + std::size_t alloc_z = l1->_shallow_size() + l1->head()->_shallow_size(); + expected_alloc_z += alloc_z; + + // replace tail to make a cycle + if (last.ptr()) { + last->assign_rest(l1); + } + } + + REQUIRE(l1->is_nil() == (v.size() == 0)); + //REQUIRE(l1->size() == v.size()); // lwill loop forever + + root_v[i] = l1; + gc->add_gc_root(reinterpret_cast(root_v[i].ptr_address())); + + REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + ++i; + } + + gc->request_gc(generation::nursery); + + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + /* verify GC preserved list structure and contents */ + for (std::size_t i = 0, n = root_v.size(); i < n; ++i) { + std::size_t nj = tc.v_.at(i).size(); + + // REQUIRE(root_v.at(i)->size() == nj); // will loop forever + if (!(root_v.at(i)->is_nil())) + REQUIRE(gc->contains(reinterpret_cast(root_v.at(i).ptr()))); + + for (std::size_t j = 0; j < nj; ++j) { + gp s = String::from(root_v.at(i)->list_ref(j)); + REQUIRE(s.ptr()); + REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); + + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::nursery); + } + + REQUIRE(root_v.at(i)->list_ref(nj).ptr() == root_v.at(i)->list_ref(0).ptr()); + } + } + } + } + } /*namespace ut*/ } /*namespace xo*/ diff --git a/xo-object/utest/String.test.cpp b/xo-object/utest/String.test.cpp index a2c3aa44..13f7847e 100644 --- a/xo-object/utest/String.test.cpp +++ b/xo-object/utest/String.test.cpp @@ -5,6 +5,7 @@ #include "xo/object/String.hpp" #include "xo/alloc/GC.hpp" +#include "xo/alloc/ArenaAlloc.hpp" #include "xo/indentlog/scope.hpp" #include "xo/indentlog/print/quoted.hpp" #include @@ -14,6 +15,7 @@ namespace xo { using xo::gc::IAlloc; using xo::gc::GC; + using xo::gc::ArenaAlloc; using xo::gc::generation; using xo::obj::String; @@ -151,6 +153,25 @@ namespace xo { } } + TEST_CASE("String.append", "[String]") + { + const bool c_debug_flag = false; + up arena = ArenaAlloc::make("testarena", 0, 16*1024, c_debug_flag); + + Object::mm = arena.get(); + + gp s1 = String::share("the"); + gp s2 = String::share(" quick"); + + gp s3 = String::append(s1, s2); + + REQUIRE(::strcmp(s1->c_str(), "the") == 0); + REQUIRE(::strcmp(s2->c_str(), " quick") == 0); + REQUIRE(s3.ptr()); + REQUIRE(s3->length() == s1->length() + s2->length()); + REQUIRE(::strcmp(s1->c_str(), "the quick")); + } + } /*namespace ut*/ } /*namespace xo*/ diff --git a/xo-ordinaltree/cmake/xo-bootstrap-macros.cmake b/xo-ordinaltree/cmake/xo-bootstrap-macros.cmake index 96592216..694d9b5c 100644 --- a/xo-ordinaltree/cmake/xo-bootstrap-macros.cmake +++ b/xo-ordinaltree/cmake/xo-bootstrap-macros.cmake @@ -11,4 +11,4 @@ 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-project-macros) +include(xo_macros/xo_cxx)