diff --git a/phlex/core/framework_graph.cpp b/phlex/core/framework_graph.cpp index 54838a78c..a8e6440a4 100644 --- a/phlex/core/framework_graph.cpp +++ b/phlex/core/framework_graph.cpp @@ -3,6 +3,7 @@ #include "phlex/concurrency.hpp" #include "phlex/core/make_computational_edges.hpp" #include "phlex/model/product_store.hpp" +#include "phlex/utilities/bulleted_list.hpp" #include "fmt/format.h" #include "fmt/ranges.h" @@ -69,7 +70,7 @@ namespace phlex::experimental { std::size_t framework_graph::seen_cell_count(std::string const& layer_name, bool const missing_ok) const { - return hierarchy_.count_for(layer_name, missing_ok); + return hierarchy_.count_for(experimental::layer_path(layer_name), missing_ok); } std::size_t framework_graph::execution_count(std::string const& node_name) const @@ -136,7 +137,7 @@ namespace phlex::experimental { return; } throw std::runtime_error( - fmt::format("\nConfiguration errors:\n - {}", fmt::join(registration_errors_, "\n - "))); + fmt::format("\nConfiguration errors:\n{}", bulleted_list(registration_errors_))); } void framework_graph::make_filter_edges() diff --git a/phlex/core/index_router.cpp b/phlex/core/index_router.cpp index aeae3db3c..cb2c67b99 100644 --- a/phlex/core/index_router.cpp +++ b/phlex/core/index_router.cpp @@ -1,6 +1,7 @@ #include "phlex/core/index_router.hpp" #include "phlex/model/flush_gate.hpp" +#include "phlex/utilities/bulleted_list.hpp" #include "phlex/utilities/hashing.hpp" #include "fmt/std.h" @@ -14,34 +15,6 @@ using namespace phlex::experimental; -namespace { - using layer_path_t = std::vector; - - std::size_t layer_hash_for_path(layer_path_t const& layer_path) - { - std::size_t result = "job"_id.hash(); - for (auto const& layer_name : layer_path | std::views::drop(1)) { - result = hash(result, identifier{layer_name}.hash()); - } - return result; - } - - bool is_strict_prefix(layer_path_t const& candidate, layer_path_t const& other) - { - // FIXME: Use std::ranges::starts_with(other, candidate) once the compilers support it (C++23) - return candidate.size() < other.size() and - std::ranges::mismatch(other, candidate).in2 == std::ranges::end(candidate); - } - - std::string delimited_layer_path(std::string_view const layer_path) - { - if (not layer_path.starts_with("/")) { - return fmt::format("/{}", layer_path); - } - return std::string{layer_path}; - } -} - namespace phlex::experimental { //======================================================================================== @@ -57,7 +30,7 @@ namespace phlex::experimental { void put_message(data_cell_index_ptr const& index, std::size_t message_id); void put_end_token(data_cell_index_ptr const& index, flush_gate const& fc); - bool matches_exactly(std::string const& layer_path) const; + bool matches_exactly(layer_path const& layer_path) const; bool is_parent_of(data_cell_index_ptr const& index) const; private: @@ -93,9 +66,9 @@ namespace phlex::experimental { flusher_.try_put({.index = index, .count = static_cast(fc.committed_total_count())}); } - bool multilayer_slot::matches_exactly(std::string const& layer_path) const + bool multilayer_slot::matches_exactly(layer_path const& layer_path) const { - return layer_path.ends_with(delimited_layer_path(static_cast(layer_))); + return layer_path.ends_with(layer_); } bool multilayer_slot::is_parent_of(data_cell_index_ptr const& index) const @@ -131,16 +104,22 @@ namespace phlex::experimental { std::vector unfold_input_layer_names, std::vector unfold_output_layer_names) { - auto sorted_layer_paths = layer_paths_from_driver; + auto sorted_layer_paths = + layer_paths_from_driver | std::views::transform([](auto const& lp) { + auto lp_as_ids = lp | + std::views::transform([](auto const& str) { return identifier(str); }) | + std::ranges::to(); + return layer_path(std::move(lp_as_ids)); + }) | + std::ranges::to>(); std::ranges::sort(sorted_layer_paths); // In sorted order, a path can only be a prefix of paths that follow it. - for (std::size_t i = 0; i < sorted_layer_paths.size(); ++i) { + for (std::size_t i = 0; i + 1 < sorted_layer_paths.size(); ++i) { bool const is_not_lowest_layer = - i + 1 < sorted_layer_paths.size() and - is_strict_prefix(sorted_layer_paths[i], sorted_layer_paths[i + 1]); + sorted_layer_paths[i].is_strict_prefix_of(sorted_layer_paths[i + 1]); if (is_not_lowest_layer) { - auto const layer_hash = layer_hash_for_path(sorted_layer_paths[i]); + auto const layer_hash = sorted_layer_paths[i].hash(); is_lowest_layer_hashes_.emplace(layer_hash, false); } } @@ -252,19 +231,17 @@ namespace phlex::experimental { return it->second; } - std::string const layerish_path{static_cast(index->layer_name())}; + layer_path const layerish_path{{index->layer_name()}}; auto broadcaster = index_set_node_for(layerish_path); index_set_node_cache_.insert({layer_hash, broadcaster}); return broadcaster; } - auto index_router::index_set_node_for(std::string const& layer_path) -> detail::index_set_node_ptr + auto index_router::index_set_node_for(layer_path const& layer_path) -> detail::index_set_node_ptr { - std::string const search_token = delimited_layer_path(layer_path); - std::vector candidates; for (auto it = index_set_nodes_.begin(), e = index_set_nodes_.end(); it != e; ++it) { - if (search_token.ends_with(delimited_layer_path(static_cast(it->first)))) { + if (layer_path.ends_with(it->first)) { candidates.push_back(it); } } @@ -277,10 +254,10 @@ namespace phlex::experimental { return nullptr; } - std::string msg = fmt::format("Multiple layers match specification {}:\n", layer_path); - for (auto const& it : candidates) { - msg += fmt::format("\n- {}", it->first); - } + std::string msg = fmt::format( + "Multiple layers match specification {}:\n{}", + layer_path, + bulleted_list(candidates | std::views::transform([](auto const& it) { return it->first; }))); throw std::runtime_error(msg); } diff --git a/phlex/core/index_router.hpp b/phlex/core/index_router.hpp index 366f1b829..086066896 100644 --- a/phlex/core/index_router.hpp +++ b/phlex/core/index_router.hpp @@ -91,7 +91,7 @@ namespace phlex::experimental { // correct for unfold outputs (the only source of unknown hashes) and consistent with // index_is_lowest_layer()'s fall-through default. bool is_lowest_layer_hash(std::size_t layer_hash) const; - detail::index_set_node_ptr index_set_node_for(std::string const& layer); + detail::index_set_node_ptr index_set_node_for(layer_path const& layer); detail::index_set_node_ptr index_set_node_for(data_cell_index_ptr const& index); std::pair multilayer_slots_for( data_cell_index_ptr const& index); diff --git a/phlex/core/input_arguments.hpp b/phlex/core/input_arguments.hpp index 350146538..34053ef3a 100644 --- a/phlex/core/input_arguments.hpp +++ b/phlex/core/input_arguments.hpp @@ -4,6 +4,7 @@ #include "phlex/core/message.hpp" #include "phlex/core/product_selector.hpp" #include "phlex/model/handle.hpp" +#include "phlex/utilities/bulleted_list.hpp" #include "fmt/format.h" @@ -32,18 +33,16 @@ namespace phlex::experimental { std::ranges::to(); if (products.empty()) { throw std::runtime_error(fmt::format( - "No products found matching the query {}\n Store (id {} from {}) contains:\n - {}", + "No products found matching the query {}\n Store (id {} from {}) contains:\n{}", query, store->index()->to_string(), store->source().to_string(), - fmt::join(all_products | views::transform(&product_specification::to_string), - "\n - "))); + bulleted_list(all_products, /*indent=*/4))); } if (products.size() > 1) { - throw std::runtime_error(fmt::format( - "Multiple products found matching the query {}:\n - {}", - query, - fmt::join(products | views::transform(&product_specification::to_string), "\n - "))); + throw std::runtime_error(fmt::format("Multiple products found matching the query {}:\n{}", + query, + bulleted_list(products, /*indent=*/4))); } return store->get_handle(products[0]); } diff --git a/phlex/core/producer_catalog.cpp b/phlex/core/producer_catalog.cpp index 1bf365705..7434a339a 100644 --- a/phlex/core/producer_catalog.cpp +++ b/phlex/core/producer_catalog.cpp @@ -1,4 +1,5 @@ #include "phlex/core/producer_catalog.hpp" +#include "phlex/utilities/bulleted_list.hpp" #include "fmt/format.h" #include "fmt/ranges.h" @@ -75,9 +76,9 @@ namespace phlex::experimental { } if (candidates.size() > 1ull) { - std::string msg = fmt::format("More than one candidate matches the query {}: \n - {}\n", + std::string msg = fmt::format("More than one candidate matches the query {}: \n{}\n", query.to_string(), - fmt::join(std::views::keys(candidates), "\n - ")); + bulleted_list(std::views::keys(candidates), /*indent=*/1)); throw std::runtime_error(msg); } diff --git a/phlex/model/CMakeLists.txt b/phlex/model/CMakeLists.txt index 85bef091d..0539e1468 100644 --- a/phlex/model/CMakeLists.txt +++ b/phlex/model/CMakeLists.txt @@ -11,6 +11,7 @@ cet_make_library( data_layer_hierarchy.cpp data_cell_index.cpp identifier.cpp + layer_path.cpp product_matcher.cpp product_store.cpp products.cpp @@ -39,6 +40,7 @@ install( data_layer_hierarchy.hpp data_cell_index.hpp identifier.hpp + layer_path.hpp product_matcher.hpp product_specification.hpp product_store.hpp diff --git a/phlex/model/data_cell_index.cpp b/phlex/model/data_cell_index.cpp index 75fb5c852..0b805871e 100644 --- a/phlex/model/data_cell_index.cpp +++ b/phlex/model/data_cell_index.cpp @@ -62,15 +62,16 @@ namespace phlex { return layer_name_; } - std::string data_cell_index::layer_path() const + experimental::layer_path data_cell_index::layer_path() const { - std::vector layers_in_reverse{std::string_view(layer_name_)}; - auto next_parent = parent(); - while (next_parent) { - layers_in_reverse.push_back(std::string_view(next_parent->layer_name())); - next_parent = next_parent->parent(); + // We know how deep we are so we can pre-allocate and fill in reverse + std::vector layers(depth_ + 1); + auto const* ptr = this; + for (auto& layer : std::views::reverse(layers)) { + layer = ptr->layer_name(); + ptr = ptr->parent_.get(); } - return fmt::format("/{}", fmt::join(std::views::reverse(layers_in_reverse), "/")); + return experimental::layer_path{std::move(layers)}; } std::size_t data_cell_index::depth() const noexcept { return depth_; } diff --git a/phlex/model/data_cell_index.hpp b/phlex/model/data_cell_index.hpp index 084baade9..3bd618bc0 100644 --- a/phlex/model/data_cell_index.hpp +++ b/phlex/model/data_cell_index.hpp @@ -5,6 +5,7 @@ #include "phlex/model/fwd.hpp" #include "phlex/model/identifier.hpp" +#include "phlex/model/layer_path.hpp" #include #include @@ -23,7 +24,7 @@ namespace phlex { using hash_type = std::size_t; data_cell_index_ptr make_child(std::string layer_name, std::size_t data_cell_number) const; experimental::identifier const& layer_name() const noexcept; - std::string layer_path() const; + experimental::layer_path layer_path() const; std::size_t depth() const noexcept; data_cell_index_ptr parent(experimental::identifier const& layer_name) const; data_cell_index_ptr parent() const noexcept; diff --git a/phlex/model/data_layer_hierarchy.cpp b/phlex/model/data_layer_hierarchy.cpp index 6daf692ea..a7e13e9e4 100644 --- a/phlex/model/data_layer_hierarchy.cpp +++ b/phlex/model/data_layer_hierarchy.cpp @@ -1,6 +1,8 @@ #include "phlex/model/data_layer_hierarchy.hpp" #include "phlex/model/data_cell_index.hpp" +#include "phlex/utilities/bulleted_list.hpp" + #include "fmt/format.h" #include "fmt/std.h" #include "spdlog/spdlog.h" @@ -35,32 +37,32 @@ namespace phlex::experimental { ++it->second->count; } - std::size_t data_layer_hierarchy::count_for(std::string const& layer, bool const missing_ok) const + std::size_t data_layer_hierarchy::count_for(layer_path const& layer, bool const missing_ok) const { - // The assumption is that specified layer is the component of a layer path - std::string search_token = layer; - if (not layer.starts_with("/")) { - search_token = '/' + layer; - } - + // The assumption is that specified layer is a portion of a layer path + // sufficient to uniquely identify a layer std::vector candidates; for (auto const& [_, entry] : layers_) { - if (entry->layer_path.ends_with(search_token)) { + if (entry->layer_path.ends_with(layer)) { candidates.push_back(entry.get()); } } if (candidates.empty()) { return missing_ok ? 0ull - : throw std::runtime_error("No layers match the specification " + layer); + : throw std::runtime_error( + fmt::format("No layers match the specification {}", layer)); } if (candidates.size() > 1ull) { - std::string msg{"The following data layers match the specification " + layer + ":\n"}; - for (auto const* entry : candidates) { - msg += "\n- " + entry->layer_path; - } - msg += "\n\nPlease specify the full layer path to disambiguate between them."; + std::string msg = + fmt::format("The following data layers match the specification {}:\n\n{}" + "\n\nPlease specify the full layer path to disambiguate between them.", + layer, + bulleted_list(candidates | std::views::transform([](auto const* entry) { + return entry->layer_path; + }), + /*indent=*/0)); throw std::runtime_error(msg); } diff --git a/phlex/model/data_layer_hierarchy.hpp b/phlex/model/data_layer_hierarchy.hpp index 384fb58d9..8f283c3e3 100644 --- a/phlex/model/data_layer_hierarchy.hpp +++ b/phlex/model/data_layer_hierarchy.hpp @@ -5,6 +5,7 @@ #include "phlex/model/data_cell_index.hpp" #include "phlex/model/fwd.hpp" +#include "phlex/model/layer_path.hpp" #include "oneapi/tbb/concurrent_unordered_map.h" @@ -25,7 +26,7 @@ namespace phlex::experimental { data_layer_hierarchy& operator=(data_layer_hierarchy&&) = delete; void increment_count(data_cell_index_ptr const& id); - std::size_t count_for(std::string const& layer, bool missing_ok = false) const; + std::size_t count_for(layer_path const& layer, bool missing_ok = false) const; void print() const; @@ -39,13 +40,13 @@ namespace phlex::experimental { std::string indent = {}) const; struct layer_entry { - layer_entry(identifier n, std::string path, std::size_t par_hash) : + layer_entry(identifier n, experimental::layer_path path, std::size_t par_hash) : name{std::move(n)}, layer_path{std::move(path)}, parent_hash{par_hash} { } identifier name; - std::string layer_path; + experimental::layer_path layer_path; std::size_t parent_hash; std::atomic count{}; }; diff --git a/phlex/model/fixed_hierarchy.cpp b/phlex/model/fixed_hierarchy.cpp index c293a2056..2816e97a7 100644 --- a/phlex/model/fixed_hierarchy.cpp +++ b/phlex/model/fixed_hierarchy.cpp @@ -75,7 +75,7 @@ namespace phlex { return data_cell_cursor{child, hierarchy_, driver_}; } - std::string data_cell_cursor::layer_path() const { return index_->layer_path(); } + experimental::layer_path data_cell_cursor::layer_path() const { return index_->layer_path(); } // ================================================================================ // data_cell_yielder implementation diff --git a/phlex/model/fixed_hierarchy.hpp b/phlex/model/fixed_hierarchy.hpp index 943785a28..370246603 100644 --- a/phlex/model/fixed_hierarchy.hpp +++ b/phlex/model/fixed_hierarchy.hpp @@ -4,6 +4,7 @@ #include "phlex/phlex_model_export.hpp" #include "phlex/model/fwd.hpp" +#include "phlex/model/layer_path.hpp" #include #include @@ -25,7 +26,7 @@ namespace phlex { // data-cell index to the underlying driver, returning a data_cell_cursor for the child. data_cell_cursor yield_child(std::string const& layer_name, std::size_t number) const; - std::string layer_path() const; + experimental::layer_path layer_path() const; private: friend class fixed_hierarchy; diff --git a/phlex/model/handle.hpp b/phlex/model/handle.hpp index 1ee4ff5bc..7acb768f8 100644 --- a/phlex/model/handle.hpp +++ b/phlex/model/handle.hpp @@ -95,7 +95,7 @@ namespace phlex { } std::string_view suffix() const noexcept { return std::string_view(suffix_); } std::string_view layer() const noexcept { return std::string_view(id_->layer_name()); } - std::string layer_path() const { return id_->layer_path(); } + std::string layer_path() const { return id_->layer_path().to_string(); } template friend class handle; diff --git a/phlex/model/identifier.hpp b/phlex/model/identifier.hpp index d99757ab0..0132396be 100644 --- a/phlex/model/identifier.hpp +++ b/phlex/model/identifier.hpp @@ -79,6 +79,7 @@ namespace phlex::experimental { // Really trying to avoid the extra function call here inline std::string_view format_as(identifier const& id) { return std::string_view(id); } + inline std::size_t hash_value(identifier const& id) { return id.hash(); } } template <> diff --git a/phlex/model/layer_path.cpp b/phlex/model/layer_path.cpp new file mode 100644 index 000000000..2cf7e2d03 --- /dev/null +++ b/phlex/model/layer_path.cpp @@ -0,0 +1,73 @@ +#include "layer_path.hpp" + +#include "boost/container_hash/hash.hpp" +#include "fmt/format.h" +#include "fmt/ranges.h" + +#include +#include + +using namespace std::literals; +using namespace phlex::experimental::literals; + +namespace phlex::experimental { + layer_path::layer_path(std::string_view path) : + layer_path_{ + std::from_range, + path | std::views::split('/') | + std::views::filter([](auto const& sr) { return not sr.empty(); }) | + std::views::transform([](auto const& sr) { return identifier(std::string_view(sr)); })} + { + if (path.starts_with("/") and not complete()) { + throw std::runtime_error( + fmt::format("A complete layer path must start with '/job'. '{}' does not!", path)); + } + } + + bool layer_path::empty() const noexcept { return layer_path_.empty(); } + + bool layer_path::complete() const noexcept { return not empty() and layer_path_[0] == "job"_idq; } + + bool layer_path::is_strict_prefix_of(layer_path const& other) const noexcept + { + if (layer_path_.size() >= other.layer_path_.size()) { + // Optimization, and address Codex observation that a path is not a _strict_ prefix of itself + return false; + } + // starts_with / ends_with aren't supported until libstdc++ *16* + // return std::ranges::starts_with(other.layer_path_, layer_path_); + auto const& [it, other_it] = std::ranges::mismatch(layer_path_, other.layer_path_); + return it == layer_path_.end(); + } + + bool layer_path::ends_with(layer_path const& other) const noexcept + { + // starts_with / ends_with aren't supported until libstdc++ *16* + // return std::ranges::ends_with(layer_path_, other.layer_path_); + auto rev_layer_path = std::views::reverse(layer_path_); + auto rev_other_layer_path = std::views::reverse(other.layer_path_); + auto const& [it, other_it] = std::ranges::mismatch(rev_layer_path, rev_other_layer_path); + return other_it == rev_other_layer_path.end(); + } + bool layer_path::ends_with(identifier const& name) const noexcept + { + return not empty() and layer_path_.back() == name; + } + + std::string layer_path::to_string() const + { + return fmt::format("{}{}", complete() ? "/" : "", fmt::join(layer_path_, "/")); + } + std::size_t layer_path::hash() const noexcept + { + if (empty()) { + return 0; + } + if (layer_path_.size() == 1) { + return layer_path_[0].hash(); + } + std::size_t seed = layer_path_[0].hash(); + boost::hash_range(seed, layer_path_.begin() + 1, layer_path_.end()); + return seed; + } +} diff --git a/phlex/model/layer_path.hpp b/phlex/model/layer_path.hpp new file mode 100644 index 000000000..fdbc70553 --- /dev/null +++ b/phlex/model/layer_path.hpp @@ -0,0 +1,61 @@ +#ifndef PHLEX_MODEL_LAYER_PATH_HPP +#define PHLEX_MODEL_LAYER_PATH_HPP +#include "phlex/model/identifier.hpp" +#include "phlex/phlex_model_export.hpp" + +#include +#include +#include +#include + +namespace phlex::experimental { + class PHLEX_MODEL_EXPORT layer_path { + public: + layer_path() = default; + layer_path(std::vector const& path) : layer_path_{path} {} + layer_path(std::vector&& path) : layer_path_{std::move(path)} {} + layer_path(std::string_view path); + + template + requires std::constructible_from && (!std::same_as) + layer_path(T const& path) : layer_path(std::string_view(path)) + { + } + + auto operator<=>(layer_path const&) const noexcept = default; + + bool empty() const noexcept; + + /// Is this path complete (does it start with "job") + bool complete() const noexcept; + + /// Is this path a strict prefix of other + bool is_strict_prefix_of(layer_path const& other) const noexcept; + + /// Does this path end with other + bool ends_with(layer_path const& other) const noexcept; + + /// Does this path identify a layer called name + bool ends_with(identifier const& name) const noexcept; + + /// Convert to string + std::string to_string() const; + + /// Hash + std::size_t hash() const noexcept; + + private: + std::vector layer_path_; + }; + + inline std::string format_as(layer_path const& lp) { return lp.to_string(); } + inline std::size_t hash_value(layer_path const& lp) { return lp.hash(); } + + // Required by catch2 + inline std::ostream& operator<<(std::ostream& os, layer_path const& lp) + { + return os << lp.to_string(); + } +} + +#endif // PHLEX_MODEL_LAYER_PATH_HPP diff --git a/phlex/utilities/CMakeLists.txt b/phlex/utilities/CMakeLists.txt index 9f3fca0db..c9878734e 100644 --- a/phlex/utilities/CMakeLists.txt +++ b/phlex/utilities/CMakeLists.txt @@ -16,6 +16,7 @@ install( FILES resumable_driver.hpp hashing.hpp + bulleted_list.hpp max_allowed_parallelism.hpp resource_usage.hpp simple_ptr_map.hpp diff --git a/phlex/utilities/bulleted_list.hpp b/phlex/utilities/bulleted_list.hpp new file mode 100644 index 000000000..6ec689f16 --- /dev/null +++ b/phlex/utilities/bulleted_list.hpp @@ -0,0 +1,45 @@ +#ifndef PHLEX_UTILITIES_BULLETED_LIST_HPP +#define PHLEX_UTILITIES_BULLETED_LIST_HPP + +#include +#include + +#include +#include + +namespace phlex::experimental { + namespace detail { + template + concept range_of_formattable = fmt::formattable>; + + template + concept range_of_to_stringable = requires(std::ranges::range_value_t const& val) { + { val.to_string() } -> std::same_as; + }; + } + + template + std::string bulleted_list(R const& rng, std::size_t indent = 2) + { + std::string prefix = + fmt::format("{blank:{indent}s}- ", fmt::arg("blank", ""), fmt::arg("indent", indent)); + std::string prefix_with_newline = fmt::format("\n{}", prefix); + return fmt::format("{}{}", prefix, fmt::join(rng, prefix_with_newline)); + } + + template + requires(!detail::range_of_formattable) + std::string bulleted_list(R const& rng, std::size_t indent = 2) + { + std::string prefix = + fmt::format("{blank:{indent}s}- ", fmt::arg("blank", ""), fmt::arg("indent", indent)); + std::string prefix_with_newline = fmt::format("\n{}", prefix); + return fmt::format( + "{}{}", + prefix, + fmt::join(rng | std::views::transform([](auto&& v) { return v.to_string(); }), + prefix_with_newline)); + } +} + +#endif // PHLEX_UTILITIES_BULLETED_LIST_HPP diff --git a/plugins/layer_generator.cpp b/plugins/layer_generator.cpp index 96a5f08e7..2b9fd3abd 100644 --- a/plugins/layer_generator.cpp +++ b/plugins/layer_generator.cpp @@ -133,11 +133,13 @@ namespace phlex::experimental { index_generator layer_generator::execute(data_cell_index_ptr const cell) { - auto it = parent_to_children_.find(cell->layer_path()); + // Used in drivers which are close to public API --> easier to stick to strings + auto cell_lp = cell->layer_path().to_string(); + auto it = parent_to_children_.find(cell_lp); assert(it != parent_to_children_.cend()); for (auto const& child : it->second) { - auto const full_child_path = cell->layer_path() + "/" + child; + auto const full_child_path = fmt::format("{}/{}", cell_lp, child); auto const& [_, total_per_parent, starting_value] = layers_.at(full_child_path); bool const has_children = parent_to_children_.contains(full_child_path); for (unsigned int i : std::views::iota(starting_value, total_per_parent + starting_value)) { diff --git a/test/core_misc_test.cpp b/test/core_misc_test.cpp index 259aebe45..ba6b60494 100644 --- a/test/core_misc_test.cpp +++ b/test/core_misc_test.cpp @@ -3,6 +3,7 @@ #include "phlex/core/glue.hpp" #include "phlex/core/registrar.hpp" #include "phlex/model/algorithm_name.hpp" +#include "phlex/utilities/bulleted_list.hpp" #include #include @@ -45,6 +46,12 @@ TEST_CASE("algorithm_name tests", "[model]") CHECK(an.match(algorithm_name::create("p:a"))); CHECK_FALSE(an.match(algorithm_name::create("p:b"))); } + SECTION("Bulleted list of algorithm_name") + { + std::vector ans = {algorithm_name::create("p:a1"), + algorithm_name::create("p:a2")}; + CHECK(bulleted_list(ans) == " - p:a1\n - p:a2"); + } } TEST_CASE("consumer tests", "[core]")