You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by cm...@apache.org on 2024/03/01 17:28:48 UTC
(trafficserver) branch 10.0.x updated: Add libswoc unit tests (#11106)
This is an automated email from the ASF dual-hosted git repository.
cmcfarlen pushed a commit to branch 10.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/10.0.x by this push:
new 5e6c7cdffe Add libswoc unit tests (#11106)
5e6c7cdffe is described below
commit 5e6c7cdffe0c20c1591e772ffaf9f69afa837c5e
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Thu Feb 29 13:07:25 2024 -0600
Add libswoc unit tests (#11106)
Before this PR, we had pulled in libswoc's production source code into ATS. But we had not done the same with the libswoc unit tests. This ports libswoc unit_tests into ATS and ties it into our cmake system so that they are built and run as a part of ATS unit tests.
(cherry picked from commit 20bae7ff5c8bff342fc1911402be8a0e84c7a179)
---
lib/swoc/CMakeLists.txt | 4 +
lib/swoc/include/swoc/IntrusiveHashMap.h | 9 +
lib/swoc/include/swoc/TextView.h | 2 +-
lib/swoc/unit_tests/CMakeLists.txt | 51 +
lib/swoc/unit_tests/ex_IntrusiveDList.cc | 215 +++
lib/swoc/unit_tests/ex_Lexicon.cc | 130 ++
lib/swoc/unit_tests/ex_MemArena.cc | 224 +++
lib/swoc/unit_tests/ex_TextView.cc | 319 ++++
lib/swoc/unit_tests/ex_UnitParser.cc | 183 +++
lib/swoc/unit_tests/ex_bw_format.cc | 691 ++++++++
lib/swoc/unit_tests/ex_ipspace_properties.cc | 639 ++++++++
lib/swoc/unit_tests/examples/resolver.txt | 21 +
lib/swoc/unit_tests/test_BufferWriter.cc | 506 ++++++
lib/swoc/unit_tests/test_Errata.cc | 430 +++++
lib/swoc/unit_tests/test_IntrusiveDList.cc | 272 ++++
lib/swoc/unit_tests/test_IntrusiveHashMap.cc | 274 ++++
lib/swoc/unit_tests/test_Lexicon.cc | 276 ++++
lib/swoc/unit_tests/test_MemArena.cc | 663 ++++++++
lib/swoc/unit_tests/test_MemSpan.cc | 310 ++++
lib/swoc/unit_tests/test_Scalar.cc | 257 +++
lib/swoc/unit_tests/test_TextView.cc | 651 ++++++++
lib/swoc/unit_tests/test_Vectray.cc | 82 +
lib/swoc/unit_tests/test_bw_format.cc | 694 ++++++++
lib/swoc/unit_tests/test_ip.cc | 2186 ++++++++++++++++++++++++++
lib/swoc/unit_tests/test_meta.cc | 119 ++
lib/swoc/unit_tests/test_range.cc | 39 +
lib/swoc/unit_tests/test_swoc_file.cc | 341 ++++
lib/swoc/unit_tests/unit_test_main.cc | 39 +
lib/swoc/unit_tests/unit_tests.part | 42 +
src/proxy/http/HttpSessionManager.cc | 44 +-
30 files changed, 9693 insertions(+), 20 deletions(-)
diff --git a/lib/swoc/CMakeLists.txt b/lib/swoc/CMakeLists.txt
index 05eef1bed4..5d877aeb63 100644
--- a/lib/swoc/CMakeLists.txt
+++ b/lib/swoc/CMakeLists.txt
@@ -120,3 +120,7 @@ set_target_properties(
install(TARGETS libswoc PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/swoc)
add_library(libswoc::libswoc ALIAS libswoc)
+
+if(BUILD_TESTING)
+add_subdirectory(unit_tests)
+endif()
diff --git a/lib/swoc/include/swoc/IntrusiveHashMap.h b/lib/swoc/include/swoc/IntrusiveHashMap.h
index 201f093e71..4bbe3b6361 100644
--- a/lib/swoc/include/swoc/IntrusiveHashMap.h
+++ b/lib/swoc/include/swoc/IntrusiveHashMap.h
@@ -514,6 +514,15 @@ IntrusiveHashMap<H>::insert(value_type *v) {
if (spot != bucket->_v) {
mixed_p = true; // found some other key, it's going to be mixed.
}
+ if (spot != limit) {
+ // If an equal key was found, walk past those to insert at the upper end of the range.
+ do {
+ spot = H::next_ptr(spot);
+ } while (spot != limit && H::equal(key, H::key_of(spot)));
+ if (spot != limit) { // something not equal past last equivalent, it's going to be mixed.
+ mixed_p = true;
+ }
+ }
_list.insert_before(spot, v);
if (spot == bucket->_v) { // added before the bucket start, update the start.
diff --git a/lib/swoc/include/swoc/TextView.h b/lib/swoc/include/swoc/TextView.h
index 01ddb659cd..9e914c8a63 100644
--- a/lib/swoc/include/swoc/TextView.h
+++ b/lib/swoc/include/swoc/TextView.h
@@ -1384,7 +1384,7 @@ TextView::suffix(int n) const noexcept {
inline TextView
TextView::suffix_at(char c) const {
self_type zret;
- if (auto n = this->rfind(c); n != npos) {
+ if (auto n = this->rfind(c); n != npos && n + 1 < this->size()) {
++n;
zret.assign(this->data() + n, this->size() - n);
}
diff --git a/lib/swoc/unit_tests/CMakeLists.txt b/lib/swoc/unit_tests/CMakeLists.txt
new file mode 100644
index 0000000000..bd2fe05269
--- /dev/null
+++ b/lib/swoc/unit_tests/CMakeLists.txt
@@ -0,0 +1,51 @@
+cmake_minimum_required(VERSION 3.12)
+project(test_libswoc CXX)
+set(CMAKE_CXX_STANDARD 17)
+
+add_executable(
+ test_libswoc
+ unit_test_main.cc
+ test_BufferWriter.cc
+ test_bw_format.cc
+ test_Errata.cc
+ test_IntrusiveDList.cc
+ test_IntrusiveHashMap.cc
+ test_ip.cc
+ test_Lexicon.cc
+ test_MemSpan.cc
+ test_MemArena.cc
+ test_meta.cc
+ test_range.cc
+ test_TextView.cc
+ test_Scalar.cc
+ test_swoc_file.cc
+ test_Vectray.cc
+ ex_bw_format.cc
+ ex_IntrusiveDList.cc
+ ex_ipspace_properties.cc
+ ex_Lexicon.cc
+ ex_MemArena.cc
+ ex_TextView.cc
+ ex_UnitParser.cc
+)
+
+target_link_libraries(test_libswoc PUBLIC libswoc PRIVATE catch2::catch2)
+set_target_properties(test_libswoc PROPERTIES CLANG_FORMAT_DIRS ${CMAKE_CURRENT_SOURCE_DIR})
+if(CMAKE_COMPILER_IS_GNUCXX)
+ target_compile_options(
+ test_libswoc
+ PRIVATE -Wall
+ -Wextra
+ -Werror
+ -Wno-unused-parameter
+ -Wno-format-truncation
+ -Wno-stringop-overflow
+ -Wno-invalid-offsetof
+ )
+ # stop the compiler from complaining about unused variable in structured binding
+ if(GCC_VERSION VERSION_LESS 8.0)
+ target_compile_options(test_libswoc PRIVATE -Wno-unused-variable)
+ endif()
+endif()
+
+add_test(NAME test_libswoc COMMAND test_libswoc)
diff --git a/lib/swoc/unit_tests/ex_IntrusiveDList.cc b/lib/swoc/unit_tests/ex_IntrusiveDList.cc
new file mode 100644
index 0000000000..3c59a9ed47
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_IntrusiveDList.cc
@@ -0,0 +1,215 @@
+/** @file
+
+ IntrusiveDList documentation examples.
+
+ This code is run during unit tests to verify that it compiles and runs correctly, but the primary
+ purpose of the code is for documentation, not testing per se. This means editing the file is
+ almost certain to require updating documentation references to code in this file.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <iostream>
+#include <string_view>
+#include <string>
+#include <algorithm>
+
+#include "swoc/IntrusiveDList.h"
+#include "swoc/bwf_base.h"
+
+#include "catch.hpp"
+
+using swoc::IntrusiveDList;
+
+class Message {
+ using self_type = Message; ///< Self reference type.
+
+public:
+ // Message severity level.
+ enum Severity { LVL_DEBUG, LVL_INFO, LVL_WARN, LVL_ERROR };
+
+protected:
+ std::string _text; // Text of the message.
+ Severity _severity{LVL_DEBUG};
+ int _indent{0}; // indentation level for display.
+
+ // Intrusive list support.
+ struct Linkage {
+ static self_type *&next_ptr(self_type *); // Link accessor.
+ static self_type *&prev_ptr(self_type *); // Link accessor.
+
+ self_type *_next{nullptr}; // Forward link.
+ self_type *_prev{nullptr}; // Backward link.
+ } _link;
+
+ bool is_in_list() const;
+
+ friend class Container;
+};
+
+auto
+Message::Linkage::next_ptr(self_type *that) -> self_type *& {
+ return that->_link._next;
+}
+auto
+Message::Linkage::prev_ptr(self_type *that) -> self_type *& {
+ return that->_link._prev;
+}
+
+bool
+Message::is_in_list() const {
+ return _link._next || _link._prev;
+}
+
+class Container {
+ using self_type = Container;
+ using MessageList = IntrusiveDList<Message::Linkage>;
+
+public:
+ ~Container();
+
+ template <typename... Args> self_type &debug(std::string_view fmt, Args &&...args);
+
+ size_t count() const;
+ self_type &clear();
+ Message::Severity max_severity() const;
+ void print() const;
+
+protected:
+ MessageList _msgs;
+};
+
+Container::~Container() {
+ this->clear(); // clean up memory.
+}
+
+auto
+Container::clear() -> self_type & {
+ Message *msg;
+ while (nullptr != (msg = _msgs.take_head())) {
+ delete msg;
+ }
+ _msgs.clear();
+ return *this;
+}
+
+size_t
+Container::count() const {
+ return _msgs.count();
+}
+
+template <typename... Args>
+auto
+Container::debug(std::string_view fmt, Args &&...args) -> self_type & {
+ Message *msg = new Message;
+ swoc::bwprint_v(msg->_text, fmt, std::forward_as_tuple(args...));
+ msg->_severity = Message::LVL_DEBUG;
+ _msgs.append(msg);
+ return *this;
+}
+
+Message::Severity
+Container::max_severity() const {
+ auto spot = std::max_element(_msgs.begin(), _msgs.end(),
+ [](Message const &lhs, Message const &rhs) { return lhs._severity < rhs._severity; });
+ return spot == _msgs.end() ? Message::Severity::LVL_DEBUG : spot->_severity;
+}
+
+void
+Container::print() const {
+ for (auto &&elt : _msgs) {
+ std::cout << static_cast<unsigned int>(elt._severity) << ": " << elt._text << std::endl;
+ }
+}
+
+TEST_CASE("IntrusiveDList Example", "[libswoc][IntrusiveDList]") {
+ Container container;
+
+ container.debug("This is message {}", 1);
+ REQUIRE(container.count() == 1);
+ // Destructor is checked for non-crashing as container goes out of scope.
+}
+
+struct Thing {
+ std::string _payload;
+ Thing *_next{nullptr};
+ Thing *_prev{nullptr};
+ using Linkage = swoc::IntrusiveLinkage<Thing, &Thing::_next, &Thing::_prev>;
+
+ Thing(std::string_view text) : _payload(text) {}
+};
+
+// Just for you, @maskit ! Demonstrating non-public links and subclassing.
+class PrivateThing : protected Thing {
+ using self_type = PrivateThing;
+ using super_type = Thing;
+
+public:
+ PrivateThing(std::string_view text) : super_type(text) {}
+
+ struct Linkage {
+ static self_type *&
+ next_ptr(self_type *t) {
+ return swoc::ptr_ref_cast<self_type>(t->_next);
+ }
+ static self_type *&
+ prev_ptr(self_type *t) {
+ return swoc::ptr_ref_cast<self_type>(t->_prev);
+ }
+ };
+
+ std::string const &
+ payload() const {
+ return _payload;
+ }
+};
+
+class PrivateThing2 : protected Thing {
+ using self_type = PrivateThing2;
+ using super_type = Thing;
+
+public:
+ PrivateThing2(std::string_view text) : super_type(text) {}
+ using Linkage = swoc::IntrusiveLinkageRebind<self_type, super_type::Linkage>;
+ friend Linkage;
+
+ std::string const &
+ payload() const {
+ return _payload;
+ }
+};
+
+TEST_CASE("IntrusiveDList Inheritance", "[libswoc][IntrusiveDList][example]") {
+ IntrusiveDList<PrivateThing::Linkage> priv_list;
+ for (size_t i = 1; i <= 23; ++i) {
+ swoc::LocalBufferWriter<16> w;
+ w.print("Item {}", i);
+ priv_list.append(new PrivateThing(w.view()));
+ REQUIRE(priv_list.count() == i);
+ }
+ REQUIRE(priv_list.head()->payload() == "Item 1");
+ REQUIRE(priv_list.tail()->payload() == "Item 23");
+
+ IntrusiveDList<PrivateThing2::Linkage> priv2_list;
+ for (size_t i = 1; i <= 23; ++i) {
+ swoc::LocalBufferWriter<16> w;
+ w.print("Item {}", i);
+ priv2_list.append(new PrivateThing2(w.view()));
+ REQUIRE(priv2_list.count() == i);
+ }
+ REQUIRE(priv2_list.head()->payload() == "Item 1");
+ REQUIRE(priv2_list.tail()->payload() == "Item 23");
+}
diff --git a/lib/swoc/unit_tests/ex_Lexicon.cc b/lib/swoc/unit_tests/ex_Lexicon.cc
new file mode 100644
index 0000000000..1be975e9ce
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_Lexicon.cc
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Verizon Media 2020
+/** @file
+
+ Lexicon example code.
+*/
+
+#include <bitset>
+
+#include "swoc/Lexicon.h"
+#include "swoc/swoc_file.h"
+#include "swoc/swoc_ip.h"
+#include "catch.hpp"
+
+// Example code for documentatoin
+// ---
+
+// This is the set of address flags
+// doc.1.begin
+enum class NetType {
+ EXTERNAL = 0, // 0x1
+ PROD, // 0x2
+ SECURE, // 0x4
+ EDGE, // 0x8
+ INVALID
+};
+// doc.1.end
+
+// The number of distinct flags.
+static constexpr size_t N_TYPES = size_t(NetType::INVALID);
+
+// Set up a Lexicon to convert between the enumeration and strings.
+// doc.2.begin
+swoc::Lexicon<NetType> const NetTypeNames{
+ {{NetType::EXTERNAL, "external"}, {NetType::PROD, "prod"}, {NetType::SECURE, "secure"}, {NetType::EDGE, "edge"}},
+ NetType::INVALID // default value for undefined name
+};
+// doc.2.end
+
+// A bit set for the flags.
+using Flags = std::bitset<N_TYPES>;
+
+TEST_CASE("Lexicon Example", "[libts][Lexicon]") {
+ swoc::IPSpace<Flags> space; // Space in which to store the flags.
+ // Load the file contents
+ // doc.file.begin
+ swoc::TextView text{R"(
+ 10.0.0.2-10.0.0.254,edge
+ 10.12.0.0/25,prod
+ 10.15.37.10-10.15.37.99,prod,secure
+ 172.19.0.0/22,external,secure
+ 192.168.18.0/23,external,prod
+ )"};
+ // doc.file.end
+ // doc.load.begin
+ // Process all the lines in the file.
+ while (text) {
+ auto line = text.take_prefix_at('\n').trim_if(&isspace);
+ auto addr_token = line.take_prefix_at(','); // first token is the range.
+ swoc::IPRange r{addr_token};
+ if (!r.empty()) { // empty means failed parse.
+ Flags flags;
+ while (line) { // parse out the rest of the comma separated elements
+ auto token = line.take_prefix_at(',');
+ auto idx = NetTypeNames[token];
+ if (idx != NetType::INVALID) { // one of the valid strings
+ flags.set(static_cast<int>(idx)); // set the bit
+ }
+ }
+ space.mark(r, flags); // store the flags in the spae.
+ }
+ }
+ // doc.load.end
+
+ using AddrCase = std::tuple<swoc::IPAddr, Flags>;
+ using swoc::IPAddr;
+ std::array<AddrCase, 5> AddrList = {
+ {{IPAddr{"10.0.0.6"}, 0x8},
+ {IPAddr{"172.19.3.31"}, 0x5},
+ {IPAddr{"192.168.18.19"}, 0x3},
+ {IPAddr{"10.15.37.57"}, 0x6},
+ {IPAddr{"10.12.0.126"}, 0x2}}
+ };
+
+ for (auto const &[addr, bits] : AddrList) {
+ // doc.lookup.begin
+ auto [range, flags] = *space.find(addr);
+ // doc.lookup.end
+ REQUIRE_FALSE(range.empty());
+ CHECK(flags == bits);
+ }
+ // doc.lookup.end
+}
+namespace {
+
+// doc.ctor.1.begin
+swoc::Lexicon<NetType> const Example1{
+ {{NetType::EXTERNAL, "external"}, {NetType::PROD, "prod"}, {NetType::SECURE, "secure"}, {NetType::EDGE, "edge"}},
+ "*invalid*", // default name for undefined values
+ NetType::INVALID // default value for undefined name
+};
+// doc.ctor.1.end
+
+// doc.ctor.2.begin
+swoc::Lexicon<NetType> const Example2{
+ {{NetType::EXTERNAL, "external"}, {NetType::PROD, "prod"}, {NetType::SECURE, "secure"}, {NetType::EDGE, "edge"}},
+};
+// doc.ctor.2.end
+
+// doc.ctor.3.begin
+swoc::Lexicon<NetType> Example3{
+ "*invalid*", // default name for undefined values
+ NetType::INVALID // default value for undefined name
+};
+// doc.ctor.3.end
+
+// doc.ctor.4.begin
+enum BoolTag {
+ INVALID = -1,
+ False = 0,
+ True = 1,
+};
+
+swoc::Lexicon<BoolTag> const BoolNames{
+ {{BoolTag::True, {"true", "1", "on", "enable", "Y", "yes"}}, {BoolTag::False, {"false", "0", "off", "disable", "N", "no"}}},
+ BoolTag::INVALID
+};
+// doc.ctor.4.end
+
+} // namespace
diff --git a/lib/swoc/unit_tests/ex_MemArena.cc b/lib/swoc/unit_tests/ex_MemArena.cc
new file mode 100644
index 0000000000..a6f7098e57
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_MemArena.cc
@@ -0,0 +1,224 @@
+/** @file
+
+ MemArena example code.
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <string_view>
+#include <memory>
+#include <random>
+#include "swoc/BufferWriter.h"
+#include "swoc/MemArena.h"
+#include "swoc/TextView.h"
+#include "swoc/IntrusiveHashMap.h"
+#include "swoc/bwf_base.h"
+#include "swoc/ext/HashFNV.h"
+#include "catch.hpp"
+
+using swoc::MemSpan;
+using swoc::MemArena;
+using swoc::TextView;
+using std::string_view;
+using swoc::FixedBufferWriter;
+using namespace std::literals;
+
+TextView
+localize(MemArena &arena, TextView view) {
+ auto span = arena.alloc(view.size()).rebind<char>();
+ memcpy(span, view);
+ return span;
+}
+
+template <typename T> struct Destructor {
+ void
+ operator()(T *t) {
+ t->~T();
+ }
+};
+
+void
+Destroy(MemArena *arena) {
+ arena->~MemArena();
+}
+
+TEST_CASE("MemArena inversion", "[libswoc][MemArena][example][inversion]") {
+ TextView tv{"You done messed up A-A-Ron"};
+ TextView text{"SolidWallOfCode"};
+
+ {
+ MemArena tmp;
+ MemArena *arena = tmp.make<MemArena>(std::move(tmp));
+ arena->~MemArena();
+ }
+
+ {
+ std::unique_ptr<MemArena> arena{new MemArena};
+
+ TextView local_tv = localize(*arena, tv);
+ REQUIRE(local_tv == tv);
+ REQUIRE(arena->contains(local_tv.data()));
+ }
+
+ {
+ auto destroyer = [](MemArena *arena) -> void { arena->~MemArena(); };
+
+ MemArena ta;
+
+ TextView local_tv = localize(ta, tv);
+ REQUIRE(local_tv == tv);
+ REQUIRE(ta.contains(local_tv.data()));
+
+ // 16 bytes.
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena(ta.make<MemArena>(std::move(ta)), destroyer);
+
+ REQUIRE(ta.size() == 0);
+ REQUIRE(ta.contains(local_tv.data()) == false);
+
+ REQUIRE(arena->size() >= local_tv.size());
+ REQUIRE(local_tv == tv);
+ REQUIRE(arena->contains(local_tv.data()));
+
+ TextView local_text = localize(*arena, text);
+ REQUIRE(local_text == text);
+ REQUIRE(local_tv != local_text);
+ REQUIRE(local_tv.data() != local_text.data());
+ REQUIRE(arena->contains(local_text.data()));
+ REQUIRE(arena->size() >= local_tv.size() + local_text.size());
+ }
+
+ {
+ MemArena ta;
+ // 8 bytes.
+ std::unique_ptr<MemArena, Destructor<MemArena>> arena(ta.make<MemArena>(std::move(ta)));
+ }
+
+ {
+ MemArena ta;
+ // 16 bytes
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena(ta.make<MemArena>(std::move(ta)), &Destroy);
+ }
+
+ {
+ MemArena ta;
+ // 16 bytes
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena(ta.make<MemArena>(std::move(ta)),
+ [](MemArena *arena) -> void { arena->~MemArena(); });
+ }
+
+ {
+ auto destroyer = [](MemArena *arena) -> void { arena->~MemArena(); };
+ MemArena ta;
+ // 8 bytes
+ std::unique_ptr<MemArena, decltype(destroyer)> arena(ta.make<MemArena>(std::move(ta)), destroyer);
+ }
+};
+
+template <typename... Args>
+TextView
+bw_localize(MemArena &arena, TextView const &fmt, Args &&...args) {
+ FixedBufferWriter w(arena.remnant());
+ auto arg_tuple{std::forward_as_tuple(args...)};
+ w.print_v(fmt, arg_tuple);
+ if (w.error()) {
+ FixedBufferWriter(arena.require(w.extent()).remnant()).print_v(fmt, arg_tuple);
+ }
+ return arena.alloc(w.extent()).rebind<char>();
+}
+
+TEST_CASE("MemArena example", "[libswoc][MemArena][example]") {
+ struct Thing {
+ using self_type = Thing;
+
+ int n{10};
+ std::string_view name{"name"};
+
+ self_type *_next{nullptr};
+ self_type *_prev{nullptr};
+
+ Thing() {}
+
+ Thing(int x) : n(x) {}
+
+ Thing(std::string_view const &s) : name(s) {}
+
+ Thing(int x, std::string_view s) : n(x), name(s) {}
+
+ Thing(std::string_view const &s, int x) : n(x), name(s) {}
+
+ struct Linkage : swoc::IntrusiveLinkage<self_type> {
+ static std::string_view
+ key_of(self_type *thing) {
+ return thing->name;
+ }
+
+ static uint32_t
+ hash_of(std::string_view const &s) {
+ return swoc::Hash32FNV1a().hash_immediate(swoc::transform_view_of(&toupper, s));
+ }
+
+ static bool
+ equal(std::string_view const &lhs, std::string_view const &rhs) {
+ return lhs == rhs;
+ }
+ };
+ };
+
+ MemArena arena;
+ TextView text = localize(arena, "Goofy Goober");
+
+ Thing *thing = arena.make<Thing>();
+ REQUIRE(thing->name == "name");
+ REQUIRE(thing->n == 10);
+
+ thing = arena.make<Thing>(text, 956);
+ REQUIRE(thing->name.data() == text.data());
+ REQUIRE(thing->n == 956);
+
+ // Consume most of the space left.
+ arena.alloc(arena.remaining() - 16);
+
+ FixedBufferWriter w(arena.remnant());
+ w.print("Much ado about not much text");
+ if (w.error()) {
+ FixedBufferWriter lw(arena.require(w.extent()).remnant());
+ lw.print("Much ado about not much text");
+ }
+ auto span = arena.alloc(w.extent()).rebind<char>(); // commit the memory.
+ REQUIRE(TextView(span) == "Much ado about not much text");
+
+ auto tv1 = bw_localize(arena, "Text: {} - '{}'", 956, "Additional");
+ REQUIRE(tv1 == "Text: 956 - 'Additional'");
+ REQUIRE(arena.contains(tv1.data()));
+
+ arena.clear();
+
+ using Map = swoc::IntrusiveHashMap<Thing::Linkage>;
+ Map *ihm = arena.make<Map>();
+
+ {
+ std::string key_1{"Key One"};
+ std::string key_2{"Key Two"};
+
+ ihm->insert(arena.make<Thing>(localize(arena, key_1), 1));
+ ihm->insert(arena.make<Thing>(localize(arena, key_2), 2));
+ }
+
+ thing = ihm->find("Key One");
+ REQUIRE(thing->name == "Key One");
+ REQUIRE(thing->n == 1);
+ REQUIRE(arena.contains(ihm));
+ REQUIRE(arena.contains(thing));
+ REQUIRE(arena.contains(thing->name.data()));
+};
diff --git a/lib/swoc/unit_tests/ex_TextView.cc b/lib/swoc/unit_tests/ex_TextView.cc
new file mode 100644
index 0000000000..09317521be
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_TextView.cc
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ TextView example code.
+
+ This code is run during unit tests to verify that it compiles and runs correctly, but the primary
+ purpose of the code is for documentation, not testing per se. This means editing the file is
+ almost certain to require updating documentation references to code in this file.
+*/
+
+#include <array>
+#include <functional>
+#include "swoc/swoc_file.h"
+
+#include "swoc/TextView.h"
+#include "catch.hpp"
+
+using swoc::TextView;
+using namespace swoc::literals;
+
+// CSV parsing.
+namespace {
+// Standard results array so these names can be used repeatedly.
+std::array<TextView, 6> alphabet{
+ {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"}
+};
+
+// -- doc csv start
+void
+parse_csv(TextView src, std::function<void(TextView)> const &f) {
+ while (src.ltrim_if(&isspace)) {
+ TextView token{src.take_prefix_at(',').rtrim_if(&isspace)};
+ if (token) { // skip empty tokens (double separators)
+ f(token);
+ }
+ }
+}
+// -- doc csv end
+
+// -- doc csv non-empty start
+void
+parse_csv_non_empty(TextView src, std::function<void(TextView)> const &f) {
+ TextView token;
+ while ((token = src.take_prefix_at(',').trim_if(&isspace))) {
+ f(token);
+ }
+}
+// -- doc csv non-empty end
+
+// -- doc kv start
+void
+parse_kw(TextView src, std::function<void(TextView, TextView)> const &f) {
+ while (src) {
+ TextView value{src.take_prefix_at(',').trim_if(&isspace)};
+ if (value) {
+ TextView key{value.take_prefix_at('=')};
+ // Trim any space that might have been around the '='.
+ f(key.rtrim_if(&isspace), value.ltrim_if(&isspace));
+ }
+ }
+}
+// -- doc kv end
+
+/** Return text that is representative of a file to parse.
+ * @return File-like content to parse.
+ */
+std::string_view
+get_resolver_text()
+{
+ constexpr std::string_view CONTENT = R"END(
+# Some comment
+172.16.10.10; conf=45 dcnum=31 dc=[cha=12,dca=30,nya=35,ata=39,daa=41,dnb=56,mib=61,sja=68,laa=69,swb=72,lob=103,fra=109,coa=112,amb=115,ir2=117,deb=122,frb=123,via=128,esa=133,waa=141,seb=141,rob=147,bga=147,bra=169,tpb=217,jpa=218,twb=220,hkb=222,aue=237,inc=240,sgb=245,]
+172.16.10.11; conf=45 dcnum=31 dc=[cha=17,dca=33,daa=38,nya=40,ata=41,mib=53,dnb=53,swb=63,sja=64,laa=69,lob=106,fra=110,coa=110,amb=111,frb=121,deb=122,esa=123,ir2=128,via=132,seb=139,waa=143,rob=144,bga=145,bra=159,tpb=215,hkb=215,twb=219,jpa=219,inc=226,aue=238,sgb=246,]
+172.16.10.12; conf=45 dcnum=31 dc=[cha=19,dca=33,nya=40,daa=41,ata=44,mib=52,dnb=53,sja=65,swb=68,laa=71,fra=104,lob=105,coa=110,amb=114,ir2=118,deb=119,frb=122,esa=127,via=128,seb=135,waa=137,rob=143,bga=145,bra=165,tpb=216,jpa=219,hkb=219,twb=222,inc=228,aue=229,sgb=246,]
+# Another comment followed by a blank line.
+
+172.16.10.13; conf=45 dcnum=31 dc=[cha=16,dca=30,nya=36,daa=41,ata=47,mib=51,dnb=56,swb=66,sja=66,laa=71,lob=103,coa=107,amb=109,fra=112,ir2=117,deb=118,frb=123,esa=132,via=133,waa=136,bga=141,rob=142,seb=144,bra=167,twb=205,tpb=215,jpa=223,hkb=223,aue=230,inc=233,sgb=242,]
+172.16.10.14; conf=45 dcnum=31 dc=[cha=19,dca=31,nya=37,ata=44,daa=46,dnb=47,mib=58,swb=65,sja=66,laa=70,lob=104,fra=109,amb=109,coa=112,frb=120,deb=121,ir2=122,esa=125,via=130,waa=141,rob=143,seb=145,bga=155,bra=170,tpb=219,twb=221,jpa=224,inc=227,hkb=227,aue=236,sgb=242,]
+172.16.10.15; conf=45 dcnum=31 dc=[cha=24,dca=32,nya=37,daa=38,ata=44,dnb=57,mib=64,sja=65,laa=66,swb=68,lob=100,coa=106,fra=112,amb=112,deb=116,ir2=123,esa=124,frb=125,via=128,waa=136,bga=145,rob=148,seb=151,bra=173,twb=206,jpa=217,tpb=227,aue=228,hkb=230,inc=234,sgb=247,]
+
+
+172.16.11.10; conf=45 dcnum=31 dc=[cha=23,dca=33,dnb=35,nya=39,ata=39,daa=44,mib=55,sja=63,swb=69,laa=69,lob=107,fra=110,amb=115,frb=116,ir2=121,coa=121,deb=124,esa=125,via=129,waa=141,seb=141,rob=141,bga=141,bra=163,jpa=213,twb=216,hkb=220,tpb=221,inc=221,aue=239,sgb=246,]
+172.16.11.11; conf=45 dcnum=31 dc=[cha=15,dca=31,nya=36,ata=37,daa=40,dnb=50,swb=61,mib=62,sja=66,laa=69,coa=107,fra=109,amb=113,deb=117,lob=119,ir2=122,frb=124,esa=125,via=129,waa=137,seb=141,rob=142,bga=148,bra=162,tpb=211,twb=217,jpa=219,hkb=226,inc=231,sgb=243,aue=245,]
+172.16.11.12; conf=45 dcnum=31 dc=[cha=15,dca=35,nya=36,daa=36,dnb=43,ata=47,mib=50,sja=64,laa=67,swb=69,lob=100,coa=104,amb=113,fra=114,deb=119,ir2=123,frb=123,via=126,esa=129,waa=140,seb=143,bga=148,bra=158,rob=198,jpa=206,twb=209,tpb=217,hkb=217,inc=227,aue=233,sgb=245,]
+172.16.11.13; conf=45 dcnum=31 dc=[cha=16,dca=33,nya=34,dnb=38,daa=43,ata=44,mib=57,swb=67,sja=70,laa=70,lob=103,coa=106,amb=107,fra=113,ir2=114,frb=119,deb=120,via=128,esa=130,waa=138,seb=139,bga=143,rob=145,bra=170,jpa=213,twb=219,tpb=219,hkb=224,inc=235,aue=239,sgb=248,]
+172.16.11.14; conf=45 dcnum=31 dc=[cha=18,dca=31,nya=38,daa=41,ata=42,dnb=47,mib=56,sja=65,swb=68,laa=75,lob=103,fra=109,coa=111,amb=114,frb=118,ir2=119,deb=126,via=128,esa=132,waa=136,seb=137,rob=146,bga=146,bra=161,tpb=212,jpa=216,twb=222,inc=223,hkb=224,sgb=242,aue=242,]
+172.16.11.15; conf=45 dcnum=31 dc=[cha=23,dca=32,nya=36,ata=37,daa=38,dnb=54,sja=66,swb=67,laa=67,mib=73,amb=107,lob=109,fra=109,deb=115,frb=120,coa=125,ir2=126,esa=134,via=137,seb=137,waa=141,rob=142,bga=156,bra=162,tpb=213,twb=222,jpa=224,hkb=228,aue=230,inc=233,sgb=255,]
+172.16.14.10; conf=45 dcnum=31 dc=[daa=30,ata=38,cha=43,dnb=51,dca=51,mib=54,laa=57,sja=58,nya=60,swb=69,coa=106,lob=127,fra=129,amb=133,ir2=134,deb=143,frb=146,esa=150,via=153,seb=163,rob=165,bga=165,bra=168,waa=169,tpb=204,jpa=207,aue=208,twb=213,hkb=223,sgb=239,inc=271,]
+172.16.14.11; conf=45 dcnum=31 dc=[daa=24,ata=40,cha=45,dnb=47,laa=55,mib=56,dca=56,nya=57,sja=67,swb=73,coa=111,lob=125,amb=133,ir2=138,fra=140,frb=145,deb=147,via=153,esa=155,waa=157,seb=158,bga=166,bra=171,rob=172,tpb=209,twb=213,jpa=218,hkb=218,aue=223,sgb=243,inc=270,]
+172.16.14.12; conf=45 dcnum=31 dc=[daa=33,cha=44,dnb=46,ata=48,mib=54,dca=55,nya=56,laa=56,sja=64,swb=72,coa=119,lob=127,amb=132,fra=133,ir2=137,deb=139,frb=140,esa=150,via=154,waa=159,seb=164,bga=168,rob=170,bra=170,jpa=209,twb=212,tpb=212,aue=212,hkb=220,sgb=243,inc=269,]
+172.16.14.13; conf=45 dcnum=31 dc=[daa=31,cha=43,ata=43,dca=50,mib=52,laa=54,nya=60,sja=61,dnb=61,swb=85,coa=113,lob=127,amb=134,fra=135,ir2=138,deb=144,esa=145,frb=150,waa=156,via=156,seb=166,bga=168,rob=172,bra=174,twb=208,aue=209,hkb=214,jpa=215,tpb=218,sgb=242,inc=271,]
+
+# Some more comments.
+# And a blank line at the end.
+
+)END";
+
+ return CONTENT;
+}
+
+} // namespace
+
+TEST_CASE("TextView Example CSV", "[libswoc][example][textview][csv]") {
+ char const *src = "alpha,bravo, charlie,delta , echo ,, ,foxtrot";
+ char const *src_non_empty = "alpha,bravo, charlie, delta, echo ,foxtrot";
+ int idx = 0;
+ parse_csv(src, [&](TextView tv) -> void { REQUIRE(tv == alphabet[idx++]); });
+ idx = 0;
+ parse_csv_non_empty(src_non_empty, [&](TextView tv) -> void { REQUIRE(tv == alphabet[idx++]); });
+};
+
+TEST_CASE("TextView Example KW", "[libswoc][example][textview][kw]") {
+ TextView src{"alpha=1, bravo= 2,charlie = 3, delta =4 ,echo ,, ,foxtrot=6"};
+ size_t idx = 0;
+ parse_kw(src, [&](TextView key, TextView value) -> void {
+ REQUIRE(key == alphabet[idx++]);
+ if (idx == 5) {
+ REQUIRE(!value);
+ } else {
+ REQUIRE(svtou(value) == idx);
+ }
+ });
+};
+
+// Example: streaming token parsing, with quote stripping.
+
+TEST_CASE("TextView Tokens", "[libswoc][example][textview][tokens]") {
+ auto tokenizer = [](TextView &src, char sep, bool strip_quotes_p = true) -> TextView {
+ TextView::size_type idx = 0;
+ // Characters of interest in a null terminated string.
+ char sep_list[3] = {'"', sep, 0};
+ bool in_quote_p = false;
+ while (idx < src.size()) {
+ // Next character of interest.
+ idx = src.find_first_of(sep_list, idx);
+ if (TextView::npos == idx) {
+ // no more, consume all of @a src.
+ break;
+ } else if ('"' == src[idx]) {
+ // quote, skip it and flip the quote state.
+ in_quote_p = !in_quote_p;
+ ++idx;
+ } else if (sep == src[idx]) { // separator.
+ if (in_quote_p) {
+ // quoted separator, skip and continue.
+ ++idx;
+ } else {
+ // found token, finish up.
+ break;
+ }
+ }
+ }
+
+ // clip the token from @a src and trim whitespace.
+ auto zret = src.take_prefix(idx).trim_if(&isspace);
+ if (strip_quotes_p) {
+ zret.trim('"');
+ }
+ return zret;
+ };
+
+ auto extract_tag = [](TextView src) -> TextView {
+ src.trim_if(&isspace);
+ if (src.prefix(2) == "W/"_sv) {
+ src.remove_prefix(2);
+ }
+ if (!src.empty() && *src == '"') {
+ src = (++src).take_prefix_at('"');
+ }
+ return src;
+ };
+
+ auto match = [&](TextView tag, TextView src, bool strong_p = true) -> bool {
+ if (strong_p && tag.prefix(2) == "W/"_sv) {
+ return false;
+ }
+ tag = extract_tag(tag);
+ while (src) {
+ TextView token{tokenizer(src, ',')};
+ if (!strong_p) {
+ token = extract_tag(token);
+ }
+ if (token == tag || token == "*"_sv) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Basic testing.
+ TextView src = "one, two";
+ REQUIRE(tokenizer(src, ',') == "one");
+ REQUIRE(tokenizer(src, ',') == "two");
+ REQUIRE(src.empty());
+ src = R"("one, two")"; // quotes around comma.
+ REQUIRE(tokenizer(src, ',') == "one, two");
+ REQUIRE(src.empty());
+ src = R"lol(one, "two" , "a,b ", some "a,,b" stuff, last)lol";
+ REQUIRE(tokenizer(src, ',') == "one");
+ REQUIRE(tokenizer(src, ',') == "two");
+ REQUIRE(tokenizer(src, ',') == "a,b ");
+ REQUIRE(tokenizer(src, ',') == R"lol(some "a,,b" stuff)lol");
+ REQUIRE(tokenizer(src, ',') == "last");
+ REQUIRE(src.empty());
+
+ src = R"("one, two)"; // unterminated quote.
+ REQUIRE(tokenizer(src, ',') == "one, two");
+ REQUIRE(src.empty());
+
+ src = R"lol(one, "two" , "a,b ", some "a,,b" stuff, last)lol";
+ REQUIRE(tokenizer(src, ',', false) == "one");
+ REQUIRE(tokenizer(src, ',', false) == R"q("two")q");
+ REQUIRE(tokenizer(src, ',', false) == R"q("a,b ")q");
+ REQUIRE(tokenizer(src, ',', false) == R"lol(some "a,,b" stuff)lol");
+ REQUIRE(tokenizer(src, ',', false) == "last");
+ REQUIRE(src.empty());
+
+ // Test against ETAG like data.
+ TextView tag = R"o("TAG956")o";
+ src = R"o("TAG1234", W/"TAG999", "TAG956", "TAG777")o";
+ REQUIRE(match(tag, src));
+ tag = R"o("TAG599")o";
+ REQUIRE(!match(tag, src));
+ REQUIRE(match(tag, R"o("*")o"));
+ tag = R"o("TAG999")o";
+ REQUIRE(!match(tag, src));
+ REQUIRE(match(tag, src, false));
+ tag = R"o(W/"TAG777")o";
+ REQUIRE(!match(tag, src));
+ REQUIRE(match(tag, src, false));
+ tag = "TAG1234";
+ REQUIRE(match(tag, src));
+ REQUIRE(!match(tag, {})); // don't crash on empty source list.
+ REQUIRE(!match({}, src)); // don't crash on empty tag.
+}
+
+// Example: line parsing from a file.
+
+TEST_CASE("TextView Lines", "[libswoc][example][textview][lines]") {
+ auto const content = get_resolver_text();
+ size_t n_lines = 0;
+
+ TextView src{content};
+ while (!src.empty()) {
+ auto line = src.take_prefix_at('\n').trim_if(&isspace);
+ if (line.empty() || '#' == *line) {
+ continue;
+ }
+ ++n_lines;
+ }
+ // To verify this
+ // grep -v '^$' lib/swoc/unit_tests/examples/resolver.txt | grep -v '^ *#' | wc
+ REQUIRE(n_lines == 16);
+};
+
+#include <set>
+#include "swoc/swoc_ip.h"
+
+TEST_CASE("TextView misc", "[libswoc][example][textview][misc]") {
+ auto src = " alpha.bravo.old:charlie.delta.old : echo.foxtrot.old "_tv;
+ REQUIRE("alpha.bravo" == src.take_prefix_at(':').remove_suffix_at('.').ltrim_if(&isspace));
+ REQUIRE("charlie.delta" == src.take_prefix_at(':').remove_suffix_at('.').ltrim_if(&isspace));
+ REQUIRE("echo.foxtrot" == src.take_prefix_at(':').remove_suffix_at('.').ltrim_if(&isspace));
+ REQUIRE(src.empty());
+}
+
+TEST_CASE("TextView parsing", "[libswoc][example][text][parsing]") {
+ static const std::set<std::string_view> DC_TAGS{"amb", "ata", "aue", "bga", "bra", "cha", "coa", "daa", "dca", "deb", "dnb",
+ "esa", "fra", "frb", "hkb", "inc", "ir2", "jpa", "laa", "lob", "mib", "nya",
+ "rob", "seb", "sgb", "sja", "swb", "tpb", "twb", "via", "waa"};
+ TextView parsed;
+ swoc::IP4Addr addr;
+
+ auto const data = get_resolver_text();
+ TextView content{data};
+ while (content) {
+ auto line{content.take_prefix_at('\n').trim_if(&isspace)}; // get the next line.
+ if (line.empty() || *line == '#') { // skip empty and lines starting with '#'
+ continue;
+ }
+ auto addr_txt = line.take_prefix_at(';');
+ auto conf_txt = line.ltrim_if(&isspace).take_prefix_if(&isspace);
+ auto dcnum_txt = line.ltrim_if(&isspace).take_prefix_if(&isspace);
+ auto dc_txt = line.ltrim_if(&isspace).take_prefix_if(&isspace);
+
+ // First element must be a valid IPv4 address.
+ REQUIRE(addr.load(addr_txt) == true);
+
+ // Confidence value must be an unsigned integer after the '='.
+ auto conf_value{conf_txt.split_suffix_at('=')};
+ swoc::svtou(conf_value, &parsed);
+ REQUIRE(conf_value == parsed); // valid integer
+
+ // Number of elements in @a dc_txt - verify it's an integer.
+ auto dcnum_value{dcnum_txt.split_suffix_at('=')};
+ auto dc_n = swoc::svtou(dcnum_value, &parsed);
+ REQUIRE(dcnum_value == parsed); // valid integer
+
+ // Verify the expected prefix for the DC list.
+ static constexpr TextView DC_PREFIX{"dc=["};
+ if (!dc_txt.starts_with(DC_PREFIX) || dc_txt.remove_prefix(DC_PREFIX.size()).empty() || dc_txt.back() != ']') {
+ continue;
+ }
+
+ dc_txt.rtrim("], \t"); // drop trailing brackets, commas, spaces, tabs.
+ // walk the comma separated tokens
+ unsigned dc_count = 0;
+ while (dc_txt) {
+ auto key = dc_txt.take_prefix_at(',');
+ auto value = key.take_suffix_at('=');
+ [[maybe_unused]] auto n = swoc::svtou(value, &parsed);
+ // Each element must be one of the known tags, followed by '=' and an integer.
+ REQUIRE(parsed == value); // value integer.
+ REQUIRE(DC_TAGS.find(key) != DC_TAGS.end());
+ ++dc_count;
+ }
+ REQUIRE(dc_count == dc_n);
+ };
+};
diff --git a/lib/swoc/unit_tests/ex_UnitParser.cc b/lib/swoc/unit_tests/ex_UnitParser.cc
new file mode 100644
index 0000000000..b146196131
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_UnitParser.cc
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Verizon Media 2020
+/** @file
+
+ Example parser for parsing strings that are counts with attached unit tokens.
+*/
+
+#include <ctype.h>
+#include <chrono>
+
+#include "swoc/Lexicon.h"
+#include "swoc/Errata.h"
+#include "catch.hpp"
+
+using swoc::TextView;
+using swoc::Lexicon;
+using swoc::Errata;
+using swoc::Rv;
+
+/** Parse a string that consists of counts and units.
+ *
+ * Give a set of units, each of which is a list of names and a multiplier, parse a string. The
+ * string contents must consist of (optional whitespace) with alternating counts and units,
+ * starting with a count. Each count is multiplied by the value of the subsequent unit. Optionally
+ * the parser can be set to allow counts without units, which are not multiplied.
+ *
+ * For example, if the units were [ "X", 10 ] , [ "L", 50 ] , [ "C", 100 ] , [ "M", 1000 ]
+ * then the following strings would be parsed as
+ *
+ * - "1X" : 10
+ * - "1L3X" : 80
+ * - "2C" : 200
+ * - "1M 4C 4X" : 1,440
+ * - "3M 5 C3 X" : 3,530
+ */
+class UnitParser {
+ using self_type = UnitParser; ///< Self reference type.
+public:
+ using value_type = uintmax_t; ///< Integral type returned.
+ using Units = swoc::Lexicon<value_type>; ///< Unit definition type.
+
+ /// Symbolic name for setting whether units are required.
+ static constexpr bool UNITS_REQUIRED = true;
+ /// Symbolic name for setting whether units are required.
+ static constexpr bool UNITS_NOT_REQUIRED = false;
+
+ /** Constructor.
+ *
+ * @param units A @c Lexicon of unit definitions.
+ * @param unit_required_p Whether valid input requires units on all values.
+ */
+ UnitParser(Units &&units, bool unit_required_p = true) noexcept;
+
+ /** Set whether a unit is required.
+ *
+ * @param flag @c true if a unit is required, @c false if not.
+ * @return @a this.
+ */
+ self_type &unit_required(bool flag);
+
+ /** Parse a string.
+ *
+ * @param src Input string.
+ * @return The computed value if the input it valid, or an error report.
+ */
+ Rv<value_type> operator()(swoc::TextView const &src) const noexcept;
+
+protected:
+ bool _unit_required_p = true; ///< Whether unitless values are allowed.
+ Units _units; ///< Unit definitions.
+};
+
+UnitParser::UnitParser(UnitParser::Units &&units, bool unit_required_p) noexcept
+ : _unit_required_p(unit_required_p), _units(std::move(units)) {
+ _units.set_default(value_type{0}); // Used to check for bad unit names.
+}
+
+UnitParser::self_type &
+UnitParser::unit_required(bool flag) {
+ _unit_required_p = false;
+ return *this;
+}
+
+auto
+UnitParser::operator()(swoc::TextView const &src) const noexcept -> Rv<value_type> {
+ value_type zret = 0;
+ TextView text = src; // Keep @a src around to report error offsets.
+
+ while (text.ltrim_if(&isspace)) {
+ TextView parsed;
+ auto n = swoc::svtou(text, &parsed);
+ if (parsed.empty()) {
+ return Errata("Required count not found at offset {}", text.data() - src.data());
+ } else if (n == std::numeric_limits<decltype(n)>::max()) {
+ return Errata("Count at offset {} was out of bounds", text.data() - src.data());
+ }
+ text.remove_prefix(parsed.size());
+ auto ptr = text.ltrim_if(&isspace).data(); // save for error reporting.
+ // Everything up to the next digit or whitespace.
+ auto unit = text.clip_prefix_of([](char c) { return !(isspace(c) || isdigit(c)); });
+ if (unit.empty()) {
+ if (_unit_required_p) {
+ return Errata("Required unit not found at offset {}", ptr - src.data());
+ }
+ } else {
+ auto mult = _units[unit]; // What's the multiplier?
+ if (mult == 0) {
+ return Errata("Unknown unit \"{}\" at offset {}", unit, ptr - src.data());
+ }
+ n *= mult;
+ }
+ zret += n;
+ }
+ return zret;
+}
+
+// --- Tests ---
+
+TEST_CASE("UnitParser Bytes", "[Lexicon][UnitParser]") {
+ UnitParser bytes{UnitParser::Units{{{1, {"B", "bytes"}},
+ {1024, {"K", "KB", "kilo", "kilobyte", "kilobytes"}},
+ {1048576, {"M", "MB", "mega", "megabyte", "megabytes"}},
+ {1 << 30, {"G", "GB", "giga", "gigabyte", "gigabytes"}}}},
+ UnitParser::UNITS_NOT_REQUIRED};
+
+ REQUIRE(bytes("56 bytes") == 56);
+ REQUIRE(bytes("3 kb") == 3 * (1 << 10));
+ REQUIRE(bytes("6k128bytes") == 6 * (1 << 10) + 128);
+ REQUIRE(bytes("6 k128bytes") == 6 * (1 << 10) + 128);
+ REQUIRE(bytes("6 K128 bytes") == 6 * (1 << 10) + 128);
+ REQUIRE(bytes("6 kilo 0x80 bytes") == 6 * (1 << 10) + 128);
+ REQUIRE(bytes("6kilo 0x8b bytes") == 6 * (1 << 10) + 0x8b);
+ REQUIRE(bytes("111") == 111);
+ REQUIRE(bytes("4MB") == 4 * (uintmax_t(1) << 20));
+ REQUIRE(bytes("4 giga") == 4 * (uintmax_t(1) << 30));
+ REQUIRE(bytes("10M 256K 512") == 10 * (1 << 20) + 256 * (1 << 10) + 512);
+ REQUIRE(bytes("512 256 kilobytes 10 megabytes") == 10 * (1 << 20) + 256 * (1 << 10) + 512);
+ REQUIRE(bytes("0x100000000") == 0x100000000);
+ auto result = bytes("56delain");
+ REQUIRE(result.is_ok() == false);
+ REQUIRE(result.errata().front().text() == "Unknown unit \"delain\" at offset 2");
+ result = bytes("12K delain");
+ REQUIRE(result.is_ok() == false);
+ REQUIRE(result.errata().front().text() == "Required count not found at offset 4");
+ result = bytes("99999999999999999999");
+ REQUIRE(result.is_ok() == false);
+ REQUIRE(result.errata().front().text() == "Count at offset 0 was out of bounds");
+}
+
+TEST_CASE("UnitParser Time", "[Lexicon][UnitParser]") {
+ using namespace std::chrono;
+ UnitParser time{UnitParser::Units{{{nanoseconds{1}.count(), {"ns", "nanosec", "nanoseconds"}},
+ {nanoseconds{microseconds{1}}.count(), {"us", "microsec", "microseconds"}},
+ {nanoseconds{milliseconds{1}}.count(), {"ms", "millisec", "milliseconds"}},
+ {nanoseconds{seconds{1}}.count(), {"s", "sec", "seconds"}},
+ {nanoseconds{minutes{1}}.count(), {"m", "min", "minutes"}},
+ {nanoseconds{hours{1}}.count(), {"h", "hour", "hours"}},
+ {nanoseconds{hours{24}}.count(), {"d", "day", "days"}},
+ {nanoseconds{hours{168}}.count(), {"w", "week", "weeks"}}}}};
+
+ REQUIRE(nanoseconds{time("2s")} == seconds{2});
+ REQUIRE(nanoseconds{time("1w 2days 12 hours")} == hours{168} + hours{2 * 24} + hours{12});
+ REQUIRE(nanoseconds{time("300ms")} == milliseconds{300});
+ REQUIRE(nanoseconds{time("1h30m")} == hours{1} + minutes{30});
+
+ auto result = time("1h30m10");
+ REQUIRE(result.is_ok() == false);
+ REQUIRE(result.errata().front().text() == "Required unit not found at offset 7");
+
+ auto duration = nanoseconds(time("30 minutes 12h"));
+ REQUIRE(minutes(750) == duration);
+}
+
+TEST_CASE("UnitParser Eggs", "[Lexicon][UnitParser]") {
+ const UnitParser eggs{
+ UnitParser::Units{UnitParser::Units::with_multi{{1, {"egg", "eggs"}}, {12, {"dozen"}}, {12 * 12, {"gross"}}}},
+ UnitParser::UNITS_NOT_REQUIRED};
+
+ REQUIRE(eggs("1") == 1);
+ REQUIRE(eggs("6") == 6);
+ REQUIRE(eggs("1 dozen") == 12);
+ REQUIRE(eggs("2 gross 6 dozen 10 eggs") == 370);
+}
diff --git a/lib/swoc/unit_tests/ex_bw_format.cc b/lib/swoc/unit_tests/ex_bw_format.cc
new file mode 100644
index 0000000000..4291bd6fa3
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_bw_format.cc
@@ -0,0 +1,691 @@
+/** @file
+
+ Unit tests for BufferFormat and bwprint.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include <chrono>
+#include <iostream>
+#include <variant>
+
+#include "swoc/MemSpan.h"
+#include "swoc/BufferWriter.h"
+#include "swoc/bwf_std.h"
+#include "swoc/bwf_ex.h"
+#include "swoc/bwf_ip.h"
+
+#include "catch.hpp"
+
+using namespace std::literals;
+using swoc::TextView;
+using swoc::BufferWriter;
+using swoc::bwf::Spec;
+using swoc::LocalBufferWriter;
+
+static constexpr TextView VERSION{"1.0.2"};
+
+TEST_CASE("BWFormat substrings", "[swoc][bwf][substr]") {
+ LocalBufferWriter<256> bw;
+ std::string_view text{"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
+
+ bw.clear().print("Text: |{0:20}|", text.substr(0, 10));
+ REQUIRE(bw.view() == "Text: |0123456789 |");
+ bw.clear().print("Text: |{:20}|", text.substr(0, 10));
+ REQUIRE(bw.view() == "Text: |0123456789 |");
+ bw.clear().print("Text: |{:20.10}|", text);
+ REQUIRE(bw.view() == "Text: |0123456789 |");
+ bw.clear().print("Text: |{0:>20}|", text.substr(0, 10));
+ REQUIRE(bw.view() == "Text: | 0123456789|");
+ bw.clear().print("Text: |{:>20}|", text.substr(0, 10));
+ REQUIRE(bw.view() == "Text: | 0123456789|");
+ bw.clear().print("Text: |{0:>20.10}|", text);
+ REQUIRE(bw.view() == "Text: | 0123456789|");
+ bw.clear().print("Text: |{0:->20}|", text.substr(9, 11));
+ REQUIRE(bw.view() == "Text: |---------9abcdefghij|");
+ bw.clear().print("Text: |{0:->20.11}|", text.substr(9));
+ REQUIRE(bw.view() == "Text: |---------9abcdefghij|");
+ bw.clear().print("Text: |{0:-<,20}|", text.substr(52, 10));
+ REQUIRE(bw.view() == "Text: |QRSTUVWXYZ|");
+}
+
+namespace {
+static constexpr std::string_view NA{"N/A"};
+
+// Define some global generators
+
+BufferWriter &
+BWF_Timestamp(BufferWriter &w, Spec const &spec) {
+ auto now = std::chrono::system_clock::now();
+ auto epoch = std::chrono::system_clock::to_time_t(now);
+ LocalBufferWriter<48> lw;
+
+ ctime_r(&epoch, lw.aux_data());
+ lw.commit(19); // take only the prefix.
+ lw.print(".{:03}", std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count() % 1000);
+ bwformat(w, spec, lw.view().substr(4));
+ return w;
+}
+
+BufferWriter &
+BWF_Now(BufferWriter &w, Spec const &spec) {
+ return swoc::bwf::Format_Integer(w, spec, std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()), false);
+}
+
+BufferWriter &
+BWF_Version(BufferWriter &w, Spec const &spec) {
+ return bwformat(w, spec, VERSION);
+}
+
+BufferWriter &
+BWF_EvilDave(BufferWriter &w, Spec const &spec) {
+ return bwformat(w, spec, "Evil Dave");
+}
+
+// Context object for several context name binding examples.
+// Hardwired for example, production coode would load values from runtime activity.
+struct Context {
+ using Fields = std::unordered_map<std::string_view, std::string_view>;
+ std::string url{"http://docs.solidwallofcode.com/libswoc/index.html?sureness=outofbounds"};
+ std::string_view host{"docs.solidwallofcode.com"};
+ std::string_view path{"/libswoc/index.html"};
+ std::string_view scheme{"http"};
+ std::string_view query{"sureness=outofbounds"};
+ std::string tls_version{"tls/1.2"};
+ std::string ip_family{"ipv4"};
+ std::string ip_remote{"172.99.80.70"};
+ Fields http_fields = {
+ {{"Host", "docs.solidwallofcode.com"},
+ {"YRP", "10.28.56.112"},
+ {"Connection", "keep-alive"},
+ {"Age", "956"},
+ {"ETag", "1337beef"}}
+ };
+ static inline std::string A{"A"};
+ static inline std::string alpha{"alpha"};
+ static inline std::string B{"B"};
+ static inline std::string bravo{"bravo"};
+ Fields cookie_fields = {
+ {{A, alpha}, {B, bravo}}
+ };
+};
+
+} // namespace
+
+void
+EX_BWF_Format_Init() {
+ swoc::bwf::Global_Names.assign("timestamp", &BWF_Timestamp);
+ swoc::bwf::Global_Names.assign("now", &BWF_Now);
+ swoc::bwf::Global_Names.assign("version", &BWF_Version);
+ swoc::bwf::Global_Names.assign("dave", &BWF_EvilDave);
+}
+
+// Work with external / global names.
+TEST_CASE("BufferWriter Example", "[bufferwriter][example]") {
+ LocalBufferWriter<256> w;
+
+ w.clear();
+ w.print("{timestamp} Test Started");
+ REQUIRE(w.view().substr(20) == "Test Started");
+ w.clear();
+ w.print("Time is {now} {now:x} {now:X} {now:#x}");
+ REQUIRE(w.size() > 12);
+}
+
+TEST_CASE("BufferWriter Context Simple", "[bufferwriter][example][context]") {
+ // Container for name bindings.
+ using CookieBinding = swoc::bwf::ContextNames<Context const>;
+
+ LocalBufferWriter<1024> w;
+
+ Context CTX;
+
+ // Generators.
+
+ auto field_gen = [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & {
+ if (auto spot = ctx.http_fields.find(spec._ext); spot != ctx.http_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ return w;
+ };
+
+ auto cookie_gen = [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & {
+ if (auto spot = ctx.cookie_fields.find(spec._ext); spot != ctx.cookie_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ return w;
+ };
+
+ // Hook up the generators.
+ CookieBinding cb;
+ cb.assign("field", field_gen);
+ cb.assign("cookie", cookie_gen);
+ cb.assign("url",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.url); });
+ cb.assign("scheme",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.scheme); });
+ cb.assign("host",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.host); });
+ cb.assign("path",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.path); });
+
+ w.print_n(cb.bind(CTX), TextView{"YRP is {field::YRP}, Cookie B is {cookie::B}."});
+ REQUIRE(w.view() == "YRP is 10.28.56.112, Cookie B is bravo.");
+ w.clear();
+ w.print_n(cb.bind(CTX), "{scheme}://{host}{path}");
+ REQUIRE(w.view() == "http://docs.solidwallofcode.com/libswoc/index.html");
+ w.clear();
+ w.print_n(cb.bind(CTX), "Potzrebie is {field::potzrebie}");
+ REQUIRE(w.view() == "Potzrebie is N/A");
+};
+
+TEST_CASE("BufferWriter Context 2", "[bufferwriter][example][context]") {
+ LocalBufferWriter<1024> w;
+
+ // Add field based access as methods to the base context.
+ struct ExContext : public Context {
+ void
+ field_gen(BufferWriter &w, Spec const &spec, TextView const &field) const {
+ if (auto spot = http_fields.find(field); spot != http_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ };
+
+ void
+ cookie_gen(BufferWriter &w, Spec const &spec, TextView const &tag) const {
+ if (auto spot = cookie_fields.find(tag); spot != cookie_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ };
+
+ } CTX;
+
+ // Container for name bindings.
+ // Override the name lookup to handle structured names.
+ class CookieBinding : public swoc::bwf::ContextNames<ExContext const> {
+ using super_type = swoc::bwf::ContextNames<ExContext const>;
+
+ public:
+ // Intercept name dispatch to check for structured names and handle those. If not structured,
+ // chain up to super class to dispatch normally.
+ BufferWriter &
+ operator()(BufferWriter &w, Spec const &spec, ExContext const &ctx) const override {
+ // Structured name prefixes.
+ static constexpr TextView FIELD_TAG{"field"};
+ static constexpr TextView COOKIE_TAG{"cookie"};
+
+ TextView name{spec._name};
+ TextView key = name.split_prefix_at('.');
+ if (key == FIELD_TAG) {
+ ctx.field_gen(w, spec, name);
+ } else if (key == COOKIE_TAG) {
+ ctx.cookie_gen(w, spec, name);
+ } else if (!key.empty()) {
+ // error case - unrecognized prefix
+ w.print("!{}!", name);
+ } else { // direct name, do normal dispatch.
+ this->super_type::operator()(w, spec, ctx);
+ }
+ return w;
+ }
+ };
+
+ // Hook up the generators.
+ CookieBinding cb;
+ cb.assign("url",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.url); });
+ cb.assign("scheme",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.scheme); });
+ cb.assign("host",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.host); });
+ cb.assign("path",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.path); });
+ cb.assign("version", BWF_Version);
+
+ w.print_n(cb.bind(CTX), "B cookie is {cookie.B}");
+ REQUIRE(w.view() == "B cookie is bravo");
+ w.clear();
+ w.print_n(cb.bind(CTX), "{scheme}://{host}{path}");
+ REQUIRE(w.view() == "http://docs.solidwallofcode.com/libswoc/index.html");
+ w.clear();
+ w.print_n(cb.bind(CTX), "Version is {version}");
+ REQUIRE(w.view() == "Version is 1.0.2");
+ w.clear();
+ w.print_n(cb.bind(CTX), "Potzrebie is {field.potzrebie}");
+ REQUIRE(w.view() == "Potzrebie is N/A");
+ w.clear();
+ w.print_n(cb.bind(CTX), "Align: |{host:<30}|");
+ REQUIRE(w.view() == "Align: |docs.solidwallofcode.com |");
+ w.clear();
+ w.print_n(cb.bind(CTX), "Align: |{host:>30}|");
+ REQUIRE(w.view() == "Align: | docs.solidwallofcode.com|");
+};
+
+namespace {
+// Alternate format string parsing.
+// This is the extractor, an instance of which is passed to the formatting logic.
+struct AltFormatEx {
+ // Construct using @a fmt as the format string.
+ AltFormatEx(TextView fmt);
+
+ // Check for remaining text to parse.
+ explicit operator bool() const;
+ // Extract the next literal and/or specifier.
+ bool operator()(std::string_view &literal, swoc::bwf::Spec &spec);
+ // This holds the format string being parsed.
+ TextView _fmt;
+};
+
+// Construct by copying a view of the format string.
+AltFormatEx::AltFormatEx(TextView fmt) : _fmt{fmt} {}
+
+// The extractor is empty if the format string is empty.
+AltFormatEx::operator bool() const {
+ return !_fmt.empty();
+}
+
+bool
+AltFormatEx::operator()(std::string_view &literal, swoc::bwf::Spec &spec) {
+ if (_fmt.size()) { // data left.
+ literal = _fmt.take_prefix_at('%');
+ if (_fmt.empty()) { // no '%' found, it's all literal, we're done.
+ return false;
+ }
+
+ if (_fmt.size() >= 1) { // Something left that's a potential specifier.
+ char c = _fmt[0];
+ if (c == '%') { // %% -> not a specifier, slap the leading % on the literal, skip the trailing.
+ literal = {literal.data(), literal.size() + 1};
+ ++_fmt;
+ } else if (c == '{') {
+ ++_fmt; // drop open brace.
+ auto style = _fmt.split_prefix_at('}');
+ if (style.empty()) {
+ throw std::invalid_argument("Unclosed open brace");
+ }
+ spec.parse(style); // stuff between the braces
+ if (spec._name.empty()) { // no format args, must have a name to be useable.
+ throw std::invalid_argument("No name in specifier");
+ }
+ // Check for structured name - put the tag in _name and the value in _ext if found.
+ TextView name{spec._name};
+ auto key = name.split_prefix_at('.');
+ if (key) {
+ spec._ext = name;
+ spec._name = key;
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+TEST_CASE("bwf alternate syntax", "[libswoc][bwf][alternate]") {
+ using BW = BufferWriter;
+ using AltNames = swoc::bwf::ContextNames<Context>;
+ AltNames names;
+ Context CTX;
+ LocalBufferWriter<256> w;
+
+ names.assign("tls", [](BW &w, Spec const &spec, Context &ctx) -> BW & { return ::swoc::bwformat(w, spec, ctx.tls_version); });
+ names.assign("proto", [](BW &w, Spec const &spec, Context &ctx) -> BW & { return ::swoc::bwformat(w, spec, ctx.ip_family); });
+ names.assign("chi", [](BW &w, Spec const &spec, Context &ctx) -> BW & { return ::swoc::bwformat(w, spec, ctx.ip_remote); });
+ names.assign("url",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.url); });
+ names.assign("scheme", [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & {
+ return bwformat(w, spec, ctx.scheme);
+ });
+ names.assign("host",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.host); });
+ names.assign("path",
+ [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & { return bwformat(w, spec, ctx.path); });
+
+ names.assign("field", [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & {
+ if (auto spot = ctx.http_fields.find(spec._ext); spot != ctx.http_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ return w;
+ });
+
+ names.assign("cookie", [](BufferWriter &w, Spec const &spec, Context const &ctx) -> BufferWriter & {
+ if (auto spot = ctx.cookie_fields.find(spec._ext); spot != ctx.cookie_fields.end()) {
+ bwformat(w, spec, spot->second);
+ } else {
+ bwformat(w, spec, NA);
+ }
+ return w;
+ });
+
+ names.assign("dave", &BWF_EvilDave);
+
+ w.print_nfv(names.bind(CTX), AltFormatEx("This is chi - %{chi}"));
+ REQUIRE(w.view() == "This is chi - 172.99.80.70");
+ w.clear().print_nfv(names.bind(CTX), AltFormatEx("Use %% for a single"));
+ REQUIRE(w.view() == "Use % for a single");
+ w.clear().print_nfv(names.bind(CTX), AltFormatEx("Use %%{proto} for %{proto}, dig?"));
+ REQUIRE(w.view() == "Use %{proto} for ipv4, dig?");
+ w.clear().print_nfv(names.bind(CTX), AltFormatEx("Width |%{proto:10}| dig?"));
+ REQUIRE(w.view() == "Width |ipv4 | dig?");
+ w.clear().print_nfv(names.bind(CTX), AltFormatEx("Width |%{proto:>10}| dig?"));
+ REQUIRE(w.view() == "Width | ipv4| dig?");
+ w.clear().print_nfv(names.bind(CTX), AltFormatEx("I hear %{dave} wants to see YRP=%{field.YRP} and cookie A is %{cookie.A}"));
+ REQUIRE(w.view() == "I hear Evil Dave wants to see YRP=10.28.56.112 and cookie A is alpha");
+}
+
+/** C / printf style formatting for BufferWriter.
+ *
+ * This is a wrapper style class, it is not for use in a persistent context. The general use pattern
+ * will be to pass a temporary instance in to the @c BufferWriter formatting. E.g
+ *
+ * @code
+ * void bwprintf(BufferWriter& w, TextView fmt, arg1, arg2, arg3, ...) {
+ * w.print_v(C_Format(fmt), std::forward_as_tuple(args));
+ * @endcode
+ */
+class C_Format {
+public:
+ /// Construct for @a fmt.
+ C_Format(TextView const &fmt);
+
+ /// Check if there is any more format to process.
+ explicit operator bool() const;
+
+ /// Get the next pieces of the format.
+ bool operator()(std::string_view &literal, Spec &spec);
+
+ /// Capture an argument use as a specifier value.
+ void capture(BufferWriter &w, Spec const &spec, std::any const &value);
+
+protected:
+ TextView _fmt; // The format string.
+ Spec _saved; // spec for which the width and/or prec is needed.
+ bool _saved_p{false}; // flag for having a saved _spec.
+ bool _prec_p{false}; // need the precision captured?
+};
+// class C_Format
+
+// ---- Implementation ----
+inline C_Format::C_Format(TextView const &fmt) : _fmt(fmt) {}
+
+// C_Format operator bool
+inline C_Format::operator bool() const {
+ return _saved_p || !_fmt.empty();
+}
+// C_Format operator bool
+
+// C_Format capture
+void
+C_Format::capture(BufferWriter &, Spec const &spec, std::any const &value) {
+ unsigned v;
+ if (typeid(int *) == value.type())
+ v = static_cast<unsigned>(*std::any_cast<int *>(value));
+ else if (typeid(unsigned *) == value.type())
+ v = *std::any_cast<unsigned *>(value);
+ else if (typeid(size_t *) == value.type())
+ v = static_cast<unsigned>(*std::any_cast<size_t *>(value));
+ else
+ return;
+
+ if (spec._ext == "w")
+ _saved._min = v;
+ if (spec._ext == "p") {
+ _saved._prec = v;
+ }
+}
+// C_Format capture
+
+// C_Format parsing
+bool
+C_Format::operator()(std::string_view &literal, Spec &spec) {
+ TextView parsed;
+
+ // clean up any old business from a previous specifier.
+ if (_prec_p) {
+ spec._type = Spec::CAPTURE_TYPE;
+ spec._ext = "p";
+ _prec_p = false;
+ return true;
+ } else if (_saved_p) {
+ spec = _saved;
+ _saved_p = false;
+ return true;
+ }
+
+ if (!_fmt.empty()) {
+ bool width_p = false;
+ literal = _fmt.take_prefix_at('%');
+ if (_fmt.empty()) {
+ return false;
+ }
+ if (!_fmt.empty()) {
+ if ('%' == *_fmt) {
+ literal = {literal.data(), literal.size() + 1};
+ ++_fmt;
+ return false;
+ }
+ }
+
+ spec._align = Spec::Align::RIGHT; // default unless overridden.
+ do {
+ char c = *_fmt;
+ if ('-' == c) {
+ spec._align = Spec::Align::LEFT;
+ } else if ('+' == c) {
+ spec._sign = Spec::SIGN_ALWAYS;
+ } else if (' ' == c) {
+ spec._sign = Spec::SIGN_NEVER;
+ } else if ('#' == c) {
+ spec._radix_lead_p = true;
+ } else if ('0' == c) {
+ spec._fill = '0';
+ } else {
+ break;
+ }
+ ++_fmt;
+ } while (!_fmt.empty());
+
+ if (_fmt.empty()) {
+ literal = TextView{literal.data(), _fmt.data()};
+ return false;
+ }
+
+ if ('*' == *_fmt) {
+ width_p = true; // signal need to capture width.
+ ++_fmt;
+ } else {
+ auto size = _fmt.size();
+ unsigned width = swoc::svto_radix<10>(_fmt);
+ if (size != _fmt.size()) {
+ spec._min = width;
+ }
+ }
+
+ if ('.' == *_fmt) {
+ ++_fmt;
+ if ('*' == *_fmt) {
+ _prec_p = true;
+ ++_fmt;
+ } else {
+ auto size = _fmt.size();
+ unsigned x = swoc::svto_radix<10>(_fmt);
+ if (size != _fmt.size()) {
+ spec._prec = x;
+ } else {
+ spec._prec = 0;
+ }
+ }
+ }
+
+ if (_fmt.empty()) {
+ literal = TextView{literal.data(), _fmt.data()};
+ return false;
+ }
+
+ char c = *_fmt++;
+ // strip length modifiers.
+ if ('l' == c || 'h' == c)
+ c = *_fmt++;
+ if ('l' == c || 'z' == c || 'j' == c || 't' == c || 'h' == c)
+ c = *_fmt++;
+
+ switch (c) {
+ case 'c':
+ spec._type = c;
+ break;
+ case 'i':
+ case 'd':
+ case 'j':
+ case 'z':
+ spec._type = 'd';
+ break;
+ case 'x':
+ case 'X':
+ spec._type = c;
+ break;
+ case 'f':
+ spec._type = 'f';
+ break;
+ case 's':
+ spec._type = 's';
+ break;
+ case 'p':
+ spec._type = c;
+ break;
+ default:
+ literal = TextView{literal.data(), _fmt.data()};
+ return false;
+ }
+ if (width_p || _prec_p) {
+ _saved_p = true;
+ _saved = spec;
+ spec = Spec::DEFAULT;
+ if (width_p) {
+ spec._type = Spec::CAPTURE_TYPE;
+ spec._ext = "w";
+ } else if (_prec_p) {
+ _prec_p = false;
+ spec._type = Spec::CAPTURE_TYPE;
+ spec._ext = "p";
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+namespace {
+template <typename... Args>
+int
+bwprintf(BufferWriter &w, TextView const &fmt, Args &&...args) {
+ size_t n = w.size();
+ w.print_nfv(swoc::bwf::NilBinding(), C_Format(fmt), swoc::bwf::ArgTuple{std::forward_as_tuple(args...)});
+ return static_cast<int>(w.size() - n);
+}
+
+} // namespace
+
+TEST_CASE("bwf printf", "[libswoc][bwf][printf]") {
+ // C_Format tests
+ LocalBufferWriter<256> w;
+
+ bwprintf(w.clear(), "Fifty Six = %d", 56);
+ REQUIRE(w.view() == "Fifty Six = 56");
+ bwprintf(w.clear(), "int is %i", 101);
+ REQUIRE(w.view() == "int is 101");
+ bwprintf(w.clear(), "int is %zd", 102);
+ REQUIRE(w.view() == "int is 102");
+ bwprintf(w.clear(), "int is %ld", 103);
+ REQUIRE(w.view() == "int is 103");
+ bwprintf(w.clear(), "int is %s", 104);
+ REQUIRE(w.view() == "int is 104");
+ bwprintf(w.clear(), "int is %ld", -105);
+ REQUIRE(w.view() == "int is -105");
+
+ TextView digits{"0123456789"};
+ bwprintf(w.clear(), "Chars |%*s|", 12, digits);
+ REQUIRE(w.view() == "Chars | 0123456789|");
+ bwprintf(w.clear(), "Chars %.*s", 4, digits);
+ REQUIRE(w.view() == "Chars 0123");
+ bwprintf(w.clear(), "Chars |%*.*s|", 12, 5, digits);
+ REQUIRE(w.view() == "Chars | 01234|");
+ // C_Format tests
+}
+
+// --- Format classes
+
+struct As_Rot13 {
+ std::string_view _src;
+
+ As_Rot13(std::string_view src) : _src{src} {}
+};
+
+BufferWriter &
+bwformat(BufferWriter &w, Spec const &spec, As_Rot13 const &wrap) {
+ static constexpr auto rot13 = [](char c) -> char {
+ return islower(c) ? (c + 13 - 'a') % 26 + 'a' : isupper(c) ? (c + 13 - 'A') % 26 + 'A' : c;
+ };
+ return bwformat(w, spec, swoc::transform_view_of(rot13, wrap._src));
+}
+
+As_Rot13
+Rotter(std::string_view const &sv) {
+ return As_Rot13(sv);
+}
+
+struct Thing {
+ std::string _name;
+ unsigned _n{0};
+};
+
+As_Rot13
+Rotter(Thing const &thing) {
+ return As_Rot13(thing._name);
+}
+
+TEST_CASE("bwf wrapper", "[libswoc][bwf][wrapper]") {
+ LocalBufferWriter<256> w;
+ std::string_view s1{"Frcvqru"};
+
+ w.clear().print("Rot {}.", As_Rot13{s1});
+ REQUIRE(w.view() == "Rot Sepideh.");
+
+ w.clear().print("Rot {}.", As_Rot13(s1));
+ REQUIRE(w.view() == "Rot Sepideh.");
+
+ w.clear().print("Rot {}.", Rotter(s1));
+ REQUIRE(w.view() == "Rot Sepideh.");
+
+ Thing thing{"Rivy Qnir", 20};
+ w.clear().print("Rot {}.", Rotter(thing));
+ REQUIRE(w.view() == "Rot Evil Dave.");
+
+ // Verify symmetry.
+ w.clear().print("Rot {}.", As_Rot13("Sepideh"));
+ REQUIRE(w.view() == "Rot Frcvqru.");
+};
diff --git a/lib/swoc/unit_tests/ex_ipspace_properties.cc b/lib/swoc/unit_tests/ex_ipspace_properties.cc
new file mode 100644
index 0000000000..8db96fefc1
--- /dev/null
+++ b/lib/swoc/unit_tests/ex_ipspace_properties.cc
@@ -0,0 +1,639 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2014 Network Geographics
+
+/** @file
+
+ Example use of IPSpace for property mapping.
+*/
+
+#include "catch.hpp"
+
+#include <memory>
+#include <limits>
+#include <iostream>
+
+#include "swoc/TextView.h"
+#include "swoc/swoc_ip.h"
+#include "swoc/bwf_ip.h"
+#include "swoc/bwf_std.h"
+
+using namespace std::literals;
+using namespace swoc::literals;
+using swoc::TextView;
+using swoc::IPEndpoint;
+
+using swoc::IP4Addr;
+using swoc::IP4Range;
+
+using swoc::IP6Addr;
+using swoc::IP6Range;
+
+using swoc::IPAddr;
+using swoc::IPRange;
+using swoc::IPSpace;
+
+using swoc::MemSpan;
+using swoc::MemArena;
+
+using W = swoc::LocalBufferWriter<256>;
+namespace {
+bool Verbose_p =
+#if VERBOSE_EXAMPLE_OUTPUT
+ true
+#else
+ false
+#endif
+ ;
+} // namespace
+
+TEST_CASE("IPSpace bitset blending", "[libswoc][ipspace][bitset][blending]") {
+ // Color each address with a set of bits.
+ using PAYLOAD = std::bitset<32>;
+ // Declare the IPSpace.
+ using Space = swoc::IPSpace<PAYLOAD>;
+ // Example data type.
+ using Data = std::tuple<TextView, PAYLOAD>;
+
+ // Dump the ranges to stdout.
+ auto dump = [](Space &space) -> void {
+ if (Verbose_p) {
+ std::cout << W().print("{} ranges\n", space.count());
+ for (auto &&[r, payload] : space) {
+ std::cout << W().print("{:25} : {}\n", r, payload);
+ }
+ }
+ };
+
+ // Convert a list of bit indices into a bitset.
+ auto make_bits = [](std::initializer_list<unsigned> indices) -> PAYLOAD {
+ PAYLOAD bits;
+ for (auto idx : indices) {
+ bits[idx] = true;
+ }
+ return bits;
+ };
+
+ // Bitset blend functor which computes a union of the bitsets.
+ auto blender = [](PAYLOAD &lhs, PAYLOAD const &rhs) -> bool {
+ lhs |= rhs;
+ return true;
+ };
+
+ // Example marking functor.
+ auto marker = [&](Space &space, swoc::MemSpan<Data> ranges) -> void {
+ // For each test range, compute the bitset from the list of bit indices.
+ for (auto &&[text, bits] : ranges) {
+ space.blend(IPRange{text}, bits, blender);
+ }
+ };
+
+ // The IPSpace instance.
+ Space space;
+
+ // test ranges 1
+ std::array<Data, 7> ranges_1 = {
+ {{"100.0.0.0-100.0.0.255", make_bits({0})},
+ {"100.0.1.0-100.0.1.255", make_bits({1})},
+ {"100.0.2.0-100.0.2.255", make_bits({2})},
+ {"100.0.3.0-100.0.3.255", make_bits({3})},
+ {"100.0.4.0-100.0.4.255", make_bits({4})},
+ {"100.0.5.0-100.0.5.255", make_bits({5})},
+ {"100.0.6.0-100.0.6.255", make_bits({6})}}
+ };
+
+ marker(space, MemSpan<Data>{ranges_1.data(), ranges_1.size()});
+ dump(space);
+
+ // test ranges 2
+ std::array<Data, 3> ranges_2 = {
+ {{"100.0.0.0-100.0.0.255", make_bits({31})},
+ {"100.0.1.0-100.0.1.255", make_bits({30})},
+ {"100.0.2.128-100.0.3.127", make_bits({29})}}
+ };
+
+ marker(space, MemSpan<Data>{ranges_2.data(), ranges_2.size()});
+ dump(space);
+
+ // test ranges 3
+ std::array<Data, 1> ranges_3 = {{{"100.0.2.0-100.0.4.255", make_bits({2, 3, 29})}}};
+
+ marker(space, MemSpan<Data>{ranges_3.data(), ranges_3.size()});
+ dump(space);
+
+ // reset blend functor
+ auto resetter = [](PAYLOAD &lhs, PAYLOAD const &rhs) -> bool {
+ auto mask = rhs;
+ lhs &= mask.flip();
+ return lhs != 0;
+ };
+
+ // erase bits
+ space.blend(IPRange{"0.0.0.0-255.255.255.255"}, make_bits({2, 3, 29}), resetter);
+ dump(space);
+
+ // ragged boundaries
+ space.blend(IPRange{"100.0.2.19-100.0.5.117"}, make_bits({16, 18, 20}), blender);
+ dump(space);
+
+ // bit list blend functor which computes a union of the bitsets.
+ auto bit_blender = [](PAYLOAD &lhs, std::initializer_list<unsigned> const &rhs) -> bool {
+ for (auto idx : rhs)
+ lhs[idx] = true;
+ return true;
+ };
+
+ std::initializer_list<unsigned> bit_list = {10, 11};
+ space.blend(IPRange{"0.0.0.1-255.255.255.254"}, bit_list, bit_blender);
+ dump(space);
+}
+
+// ---
+
+/** A "table" is conceptually a table with the rows labeled by IP address and a set of
+ * property columns that represent data for each IP address.
+ */
+class Table {
+ using self_type = Table; ///< Self reference type.
+public:
+ static constexpr char SEP = ','; /// Value separator for input file.
+
+ /** A property is the description of data for an address.
+ * The table consists of an ordered list of properties, each corresponding to a column.
+ */
+ class Property {
+ using self_type = Property; ///< Self reference type.
+ public:
+ /// A handle to an instance.
+ using Handle = std::unique_ptr<self_type>;
+
+ /** Construct an instance.
+ *
+ * @param name Property name.
+ */
+ Property(TextView const &name) : _name(name){};
+
+ /// Force virtual destructor.
+ virtual ~Property() = default;
+
+ /** The size of the property in bytes.
+ *
+ * @return The amount of data needed for a single instance of the property value.
+ */
+ virtual size_t size() const = 0;
+
+ /** The index in the table of the property.
+ *
+ * @return The column index.
+ */
+ unsigned
+ idx() const {
+ return _idx;
+ }
+
+ /** Token persistence.
+ *
+ * @return @c true if the token needs to be preserved, @c false if not.
+ *
+ * If the token for the value is consumed, this should be left as is. However, if the token
+ * itself needs to be persistent for the lifetime of the table, this must be overridden to
+ * return @c true.
+ */
+ virtual bool
+ needs_localized_token() const {
+ return false;
+ }
+
+ /// @return The row data offset in bytes for this property.
+ size_t
+ offset() const {
+ return _offset;
+ }
+
+ /** Parse the @a token.
+ *
+ * @param token Value from the input file for this property.
+ * @param span Row data storage for this property.
+ * @return @c true if @a token was correctly parse, @c false if not.
+ *
+ * The table parses the input file and handles the fields in a line. Each value is passed to
+ * the corresponding property for parsing via this method. The method should update the data
+ * pointed at by @a span.
+ */
+ virtual bool parse(TextView token, MemSpan<std::byte> span) = 0;
+
+ protected:
+ friend class Table;
+
+ TextView _name; ///< Name of the property.
+ unsigned _idx = std::numeric_limits<unsigned>::max(); ///< Column index.
+ size_t _offset = std::numeric_limits<size_t>::max(); ///< Offset into a row.
+
+ /** Set the column index.
+ *
+ * @param idx Index for this property.
+ * @return @a this.
+ *
+ * This is called from @c Table to indicate the column index.
+ */
+ self_type &
+ assign_idx(unsigned idx) {
+ _idx = idx;
+ return *this;
+ }
+
+ /** Set the row data @a offset.
+ *
+ * @param offset Offset in bytes.
+ * @return @a this
+ *
+ * This is called from @c Table to store the row data offset.
+ */
+ self_type &
+ assign_offset(size_t offset) {
+ _offset = offset;
+ return *this;
+ }
+ };
+
+ /// Construct an empty Table.
+ Table() = default;
+
+ /** Add a property column to the table.
+ *
+ * @tparam P Property class.
+ * @param col Column descriptor.
+ * @return @a A pointer to the property.
+ *
+ * The @c Property instance must be owned by the @c Table because changes are made to it specific
+ * to this instance of @c Table.
+ */
+ template <typename P> P *add_column(std::unique_ptr<P> &&col);
+
+ /// A row in the table.
+ class Row {
+ using self_type = Row; ///< Self reference type.
+ public:
+ /// Default cconstruct an row with uninitialized data.
+ Row(MemSpan<std::byte> span) : _data(span) {}
+ /** Extract property specific data from @a this.
+ *
+ * @param prop Property that defines the data.
+ * @return The range of bytes in the row for @a prop.
+ */
+ MemSpan<std::byte> span_for(Property const &prop) const;
+
+ protected:
+ MemSpan<std::byte> _data; ///< Raw row data.
+ };
+
+ /** Parse input.
+ *
+ * @param src The source to parse.
+ * @return @a true if parsing was successful, @c false if not.
+ *
+ * In general, @a src will be the contents of a file.
+ *
+ * @see swoc::file::load
+ */
+ bool parse(TextView src);
+
+ /** Look up @a addr in the table.
+ *
+ * @param addr Address to find.
+ * @return A @c Row for the address, or @c nullptr if not found.
+ */
+ Row *find(IPAddr const &addr);
+
+ /// @return The number of ranges in the container.
+ size_t
+ size() const {
+ return _space.count();
+ }
+
+ /** Property for column @a idx.
+ *
+ * @param idx Index.
+ * @return The property.
+ */
+ Property *
+ column(unsigned idx) {
+ return _columns[idx].get();
+ }
+
+protected:
+ size_t _size = 0; ///< Size of row data.
+ /// Defined properties for columns.
+ std::vector<Property::Handle> _columns;
+
+ /// IPSpace type.
+ using space = IPSpace<Row>;
+ space _space; ///< IPSpace instance.
+
+ MemArena _arena; ///< Arena for storing rows.
+
+ /** Extract the next token from the line.
+ *
+ * @param line Current line [in,out]
+ * @return Extracted token.
+ */
+ TextView token(TextView &line);
+
+ /** Localize view.
+ *
+ * @param src View to localize.
+ * @return The localized view.
+ *
+ * This copies @a src to the internal @c MemArena and returns a view of the copied data.
+ */
+ TextView localize(TextView const &src);
+};
+
+template <typename P>
+P *
+Table::add_column(std::unique_ptr<P> &&col) {
+ auto prop = col.get();
+ auto idx = _columns.size();
+ col->assign_offset(_size);
+ col->assign_idx(idx);
+ _size += static_cast<Property *>(prop)->size();
+ _columns.emplace_back(std::move(col));
+ return prop;
+}
+
+TextView
+Table::localize(TextView const &src) {
+ auto span = _arena.alloc(src.size()).rebind<char>();
+ memcpy(span, src);
+ return span;
+}
+
+TextView
+Table::token(TextView &line) {
+ TextView::size_type idx = 0;
+ // Characters of interest.
+ static char constexpr separators[2] = {'"', SEP};
+ static TextView sep_list{separators, 2};
+ bool in_quote_p = false;
+ while (idx < line.size()) {
+ // Next character of interest.
+ idx = line.find_first_of(sep_list, idx);
+ if (TextView::npos == idx) { // nothing interesting left, consume all of @a line.
+ break;
+ } else if ('"' == line[idx]) { // quote, skip it and flip the quote state.
+ in_quote_p = !in_quote_p;
+ ++idx;
+ } else if (SEP == line[idx]) { // separator.
+ if (in_quote_p) { // quoted separator, skip and continue.
+ ++idx;
+ } else { // found token, finish up.
+ break;
+ }
+ }
+ }
+
+ // clip the token from @a src and trim whitespace, quotes
+ auto zret = line.take_prefix(idx).trim_if(&isspace).trim('"');
+ return zret;
+}
+
+bool
+Table::parse(TextView src) {
+ unsigned line_no = 0;
+ while (src) {
+ auto line = src.take_prefix_at('\n').ltrim_if(&isspace);
+ ++line_no;
+ // skip blank and comment lines.
+ if (line.empty() || '#' == *line) {
+ continue;
+ }
+
+ auto range_token = line.take_prefix_at(',');
+ IPRange range{range_token};
+ if (range.empty()) {
+ std::cout << W().print("{} is not a valid range specification.", range_token);
+ continue; // This is an error, real code should report it.
+ }
+
+ auto span = _arena.alloc(_size).rebind<std::byte>(); // need this broken out.
+ Row row{span}; // store the original span to preserve it.
+ for (auto const &col : _columns) {
+ auto token = this->token(line);
+ if (col->needs_localized_token()) {
+ token = this->localize(token);
+ }
+ if (!col->parse(token, span.subspan(0, col->size()))) {
+ std::cout << W().print("Value \"{}\" at index {} on line {} is invalid.", token, col->idx(), line_no);
+ }
+ // drop reference to storage used by this column.
+ span.remove_prefix(col->size());
+ }
+ _space.mark(range, std::move(row));
+ }
+ return true;
+}
+
+auto
+Table::find(IPAddr const &addr) -> Row * {
+ auto spot = _space.find(addr);
+ return spot == _space.end() ? nullptr : &(spot->payload());
+}
+
+bool
+operator==(Table::Row const &, Table::Row const &) {
+ return false;
+}
+
+MemSpan<std::byte>
+Table::Row::span_for(Table::Property const &prop) const {
+ return _data.subspan(prop.offset(), prop.size());
+}
+
+// ---
+
+/** A set of keys, each of which represents an independent property.
+ * The set of keys must be specified at construction, keys not in the list are invalid.
+ */
+class FlagGroupProperty : public Table::Property {
+ using self_type = FlagGroupProperty; ///< Self reference type.
+ using super_type = Table::Property; ///< Parent type.
+public:
+ /** Construct with a @a name and a list of @a tags.
+ *
+ * @param name of the property
+ * @param tags List of valid tags that represent attributes.
+ *
+ * Input tokens must consist of lists of tokens, each of which is one of the @a tags.
+ * This is stored so that the exact set of tags present can be retrieved.
+ */
+ FlagGroupProperty(TextView const &name, std::initializer_list<TextView> tags);
+
+ /** Check for a tag being present.
+ *
+ * @param idx Tag index, as specified in the constructor tag list.
+ * @param row Row data from the @c Table.
+ * @return @c true if the tag was present, @c false if not.
+ */
+ bool is_set(Table::Row const &row, unsigned idx) const;
+
+protected:
+ size_t size() const override; ///< Storeage required in a row.
+
+ /** Parse a token.
+ *
+ * @param token Token to parse (list of tags).
+ * @param span Storage for parsed results.
+ * @return @c true on a successful parse, @c false if not.
+ */
+ bool parse(TextView token, MemSpan<std::byte> span) override;
+ /// List of tags.
+ std::vector<TextView> _tags;
+};
+
+/** Enumeration property.
+ * The tokens for this property are assumed to be from a limited set of tags. Each token, the
+ * value for that row, must be one of those tags. The tags do not need to be specified, but will be
+ * accumulated as needed. The property supports a maximum of 255 distinct tags.
+ */
+class EnumProperty : public Table::Property {
+ using self_type = EnumProperty; ///< Self reference type.
+ using super_type = Table::Property; ///< Parent type.
+ using store_type = __uint8_t; ///< Row storage type.
+public:
+ using super_type::super_type; ///< Inherit super type constructors.
+
+ /// @return The enumeration tag for this @a row.
+ TextView operator()(Table::Row const &row) const;
+
+protected:
+ std::vector<TextView> _tags; ///< Tags in the enumeration.
+
+ /// @a return Size of required storage.
+ size_t
+ size() const override {
+ return sizeof(store_type);
+ }
+
+ /** Parse a token.
+ *
+ * @param token Token to parse (an enumeration tag).
+ * @param span Storage for parsed results.
+ * @return @c true on a successful parse, @c false if not.
+ */
+ bool parse(TextView token, MemSpan<std::byte> span) override;
+};
+
+class StringProperty : public Table::Property {
+ using self_type = StringProperty;
+ using super_type = Table::Property;
+
+public:
+ static constexpr size_t SIZE = sizeof(TextView);
+ using super_type::super_type;
+
+protected:
+ size_t
+ size() const override {
+ return SIZE;
+ }
+ bool parse(TextView token, MemSpan<std::byte> span) override;
+ bool
+ needs_localized_token() const override {
+ return true;
+ }
+};
+
+// ---
+bool
+StringProperty::parse(TextView token, MemSpan<std::byte> span) {
+ memcpy(span.data(), &token, sizeof(token));
+ return true;
+}
+
+FlagGroupProperty::FlagGroupProperty(TextView const &name, std::initializer_list<TextView> tags) : super_type(name) {
+ _tags.reserve(tags.size());
+ for (auto const &tag : tags) {
+ _tags.emplace_back(tag);
+ }
+}
+
+bool
+FlagGroupProperty::parse(TextView token, MemSpan<std::byte> span) {
+ if ("-"_tv == token) {
+ return true;
+ } // marker for no flags.
+ memset(span, 0);
+ while (token) {
+ auto tag = token.take_prefix_at(';');
+ unsigned j = 0;
+ for (auto const &key : _tags) {
+ if (0 == strcasecmp(key, tag)) {
+ span[j / 8] |= (std::byte{1} << (j % 8));
+ break;
+ }
+ ++j;
+ }
+ if (j > _tags.size()) {
+ std::cout << W().print("Tag \"{}\" is not recognized.", tag);
+ return false;
+ }
+ }
+ return true;
+}
+
+bool
+FlagGroupProperty::is_set(Table::Row const &row, unsigned idx) const {
+ auto sp = row.span_for(*this);
+ return std::byte{0} != ((sp[idx / 8] >> (idx % 8)) & std::byte{1});
+}
+
+size_t
+FlagGroupProperty::size() const {
+ return swoc::Scalar<8>(swoc::round_up(_tags.size())).count();
+}
+
+bool
+EnumProperty::parse(TextView token, MemSpan<std::byte> span) {
+ // Already got one?
+ auto spot = std::find_if(_tags.begin(), _tags.end(), [&](TextView const &tag) { return 0 == strcasecmp(token, tag); });
+ if (spot == _tags.end()) { // nope, add it to the list.
+ _tags.push_back(token);
+ spot = std::prev(_tags.end());
+ }
+ span.rebind<uint8_t>()[0] = spot - _tags.begin();
+ return true;
+}
+
+TextView
+EnumProperty::operator()(Table::Row const &row) const {
+ auto idx = row.span_for(*this).rebind<store_type>()[0];
+ return _tags[idx];
+}
+
+// ---
+
+TEST_CASE("IPSpace properties", "[libswoc][ip][ex][properties]") {
+ Table table;
+ auto flag_names = {"prod"_tv, "dmz"_tv, "internal"_tv};
+ auto owner = table.add_column(std::make_unique<EnumProperty>("owner"));
+ auto colo = table.add_column(std::make_unique<EnumProperty>("colo"));
+ auto flags = table.add_column(std::make_unique<FlagGroupProperty>("flags"_tv, flag_names));
+ [[maybe_unused]] auto description = table.add_column(std::make_unique<StringProperty>("Description"));
+
+ TextView src = R"(10.1.1.0/24,asf,cmi,prod;internal,"ASF core net"
+192.168.28.0/25,asf,ind,prod,"Indy Net"
+192.168.28.128/25,asf,abq,dmz;internal,"Albuquerque zone"
+)";
+
+ REQUIRE(true == table.parse(src));
+ REQUIRE(3 == table.size());
+ auto row = table.find(IPAddr{"10.1.1.56"});
+ REQUIRE(nullptr != row);
+ CHECK(true == flags->is_set(*row, 0));
+ CHECK(false == flags->is_set(*row, 1));
+ CHECK(true == flags->is_set(*row, 2));
+ CHECK("asf"_tv == (*owner)(*row));
+
+ row = table.find(IPAddr{"192.168.28.131"});
+ REQUIRE(row != nullptr);
+ CHECK("abq"_tv == (*colo)(*row));
+};
diff --git a/lib/swoc/unit_tests/examples/resolver.txt b/lib/swoc/unit_tests/examples/resolver.txt
new file mode 100644
index 0000000000..75b969d5b0
--- /dev/null
+++ b/lib/swoc/unit_tests/examples/resolver.txt
@@ -0,0 +1,21 @@
+# Some comment
+172.16.10.10; conf=45 dcnum=31 dc=[cha=12,dca=30,nya=35,ata=39,daa=41,dnb=56,mib=61,sja=68,laa=69,swb=72,lob=103,fra=109,coa=112,amb=115,ir2=117,deb=122,frb=123,via=128,esa=133,waa=141,seb=141,rob=147,bga=147,bra=169,tpb=217,jpa=218,twb=220,hkb=222,aue=237,inc=240,sgb=245,]
+172.16.10.11; conf=45 dcnum=31 dc=[cha=17,dca=33,daa=38,nya=40,ata=41,mib=53,dnb=53,swb=63,sja=64,laa=69,lob=106,fra=110,coa=110,amb=111,frb=121,deb=122,esa=123,ir2=128,via=132,seb=139,waa=143,rob=144,bga=145,bra=159,tpb=215,hkb=215,twb=219,jpa=219,inc=226,aue=238,sgb=246,]
+172.16.10.12; conf=45 dcnum=31 dc=[cha=19,dca=33,nya=40,daa=41,ata=44,mib=52,dnb=53,sja=65,swb=68,laa=71,fra=104,lob=105,coa=110,amb=114,ir2=118,deb=119,frb=122,esa=127,via=128,seb=135,waa=137,rob=143,bga=145,bra=165,tpb=216,jpa=219,hkb=219,twb=222,inc=228,aue=229,sgb=246,]
+# Another comment followed by a blank line.
+
+172.16.10.13; conf=45 dcnum=31 dc=[cha=16,dca=30,nya=36,daa=41,ata=47,mib=51,dnb=56,swb=66,sja=66,laa=71,lob=103,coa=107,amb=109,fra=112,ir2=117,deb=118,frb=123,esa=132,via=133,waa=136,bga=141,rob=142,seb=144,bra=167,twb=205,tpb=215,jpa=223,hkb=223,aue=230,inc=233,sgb=242,]
+172.16.10.14; conf=45 dcnum=31 dc=[cha=19,dca=31,nya=37,ata=44,daa=46,dnb=47,mib=58,swb=65,sja=66,laa=70,lob=104,fra=109,amb=109,coa=112,frb=120,deb=121,ir2=122,esa=125,via=130,waa=141,rob=143,seb=145,bga=155,bra=170,tpb=219,twb=221,jpa=224,inc=227,hkb=227,aue=236,sgb=242,]
+172.16.10.15; conf=45 dcnum=31 dc=[cha=24,dca=32,nya=37,daa=38,ata=44,dnb=57,mib=64,sja=65,laa=66,swb=68,lob=100,coa=106,fra=112,amb=112,deb=116,ir2=123,esa=124,frb=125,via=128,waa=136,bga=145,rob=148,seb=151,bra=173,twb=206,jpa=217,tpb=227,aue=228,hkb=230,inc=234,sgb=247,]
+
+172.16.11.10; conf=45 dcnum=31 dc=[cha=23,dca=33,dnb=35,nya=39,ata=39,daa=44,mib=55,sja=63,swb=69,laa=69,lob=107,fra=110,amb=115,frb=116,ir2=121,coa=121,deb=124,esa=125,via=129,waa=141,seb=141,rob=141,bga=141,bra=163,jpa=213,twb=216,hkb=220,tpb=221,inc=221,aue=239,sgb=246,]
+172.16.11.11; conf=45 dcnum=31 dc=[cha=15,dca=31,nya=36,ata=37,daa=40,dnb=50,swb=61,mib=62,sja=66,laa=69,coa=107,fra=109,amb=113,deb=117,lob=119,ir2=122,frb=124,esa=125,via=129,waa=137,seb=141,rob=142,bga=148,bra=162,tpb=211,twb=217,jpa=219,hkb=226,inc=231,sgb=243,aue=245,]
+172.16.11.12; conf=45 dcnum=31 dc=[cha=15,dca=35,nya=36,daa=36,dnb=43,ata=47,mib=50,sja=64,laa=67,swb=69,lob=100,coa=104,amb=113,fra=114,deb=119,ir2=123,frb=123,via=126,esa=129,waa=140,seb=143,bga=148,bra=158,rob=198,jpa=206,twb=209,tpb=217,hkb=217,inc=227,aue=233,sgb=245,]
+172.16.11.13; conf=45 dcnum=31 dc=[cha=16,dca=33,nya=34,dnb=38,daa=43,ata=44,mib=57,swb=67,sja=70,laa=70,lob=103,coa=106,amb=107,fra=113,ir2=114,frb=119,deb=120,via=128,esa=130,waa=138,seb=139,bga=143,rob=145,bra=170,jpa=213,twb=219,tpb=219,hkb=224,inc=235,aue=239,sgb=248,]
+172.16.11.14; conf=45 dcnum=31 dc=[cha=18,dca=31,nya=38,daa=41,ata=42,dnb=47,mib=56,sja=65,swb=68,laa=75,lob=103,fra=109,coa=111,amb=114,frb=118,ir2=119,deb=126,via=128,esa=132,waa=136,seb=137,rob=146,bga=146,bra=161,tpb=212,jpa=216,twb=222,inc=223,hkb=224,sgb=242,aue=242,]
+172.16.11.15; conf=45 dcnum=31 dc=[cha=23,dca=32,nya=36,ata=37,daa=38,dnb=54,sja=66,swb=67,laa=67,mib=73,amb=107,lob=109,fra=109,deb=115,frb=120,coa=125,ir2=126,esa=134,via=137,seb=137,waa=141,rob=142,bga=156,bra=162,tpb=213,twb=222,jpa=224,hkb=228,aue=230,inc=233,sgb=255,]
+172.16.14.10; conf=45 dcnum=31 dc=[daa=30,ata=38,cha=43,dnb=51,dca=51,mib=54,laa=57,sja=58,nya=60,swb=69,coa=106,lob=127,fra=129,amb=133,ir2=134,deb=143,frb=146,esa=150,via=153,seb=163,rob=165,bga=165,bra=168,waa=169,tpb=204,jpa=207,aue=208,twb=213,hkb=223,sgb=239,inc=271,]
+172.16.14.11; conf=45 dcnum=31 dc=[daa=24,ata=40,cha=45,dnb=47,laa=55,mib=56,dca=56,nya=57,sja=67,swb=73,coa=111,lob=125,amb=133,ir2=138,fra=140,frb=145,deb=147,via=153,esa=155,waa=157,seb=158,bga=166,bra=171,rob=172,tpb=209,twb=213,jpa=218,hkb=218,aue=223,sgb=243,inc=270,]
+172.16.14.12; conf=45 dcnum=31 dc=[daa=33,cha=44,dnb=46,ata=48,mib=54,dca=55,nya=56,laa=56,sja=64,swb=72,coa=119,lob=127,amb=132,fra=133,ir2=137,deb=139,frb=140,esa=150,via=154,waa=159,seb=164,bga=168,rob=170,bra=170,jpa=209,twb=212,tpb=212,aue=212,hkb=220,sgb=243,inc=269,]
+172.16.14.13; conf=45 dcnum=31 dc=[daa=31,cha=43,ata=43,dca=50,mib=52,laa=54,nya=60,sja=61,dnb=61,swb=85,coa=113,lob=127,amb=134,fra=135,ir2=138,deb=144,esa=145,frb=150,waa=156,via=156,seb=166,bga=168,rob=172,bra=174,twb=208,aue=209,hkb=214,jpa=215,tpb=218,sgb=242,inc=271,]
+
diff --git a/lib/swoc/unit_tests/test_BufferWriter.cc b/lib/swoc/unit_tests/test_BufferWriter.cc
new file mode 100644
index 0000000000..9eeee19ae4
--- /dev/null
+++ b/lib/swoc/unit_tests/test_BufferWriter.cc
@@ -0,0 +1,506 @@
+/** @file
+
+ Unit tests for BufferWriter.h.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include <cstring>
+#include "swoc/MemSpan.h"
+#include "swoc/TextView.h"
+#include "swoc/MemArena.h"
+#include "swoc/BufferWriter.h"
+#include "swoc/ArenaWriter.h"
+#include "catch.hpp"
+
+using swoc::TextView;
+using swoc::MemSpan;
+
+namespace {
+std::string_view three[] = {"a", "", "bcd"};
+}
+
+TEST_CASE("BufferWriter::write(StringView)", "[BWWSV]") {
+ class X : public swoc::BufferWriter {
+ size_t i, j;
+
+ public:
+ bool good;
+
+ X() : i(0), j(0), good(true) {}
+
+ X &
+ write(char c) override {
+ while (j == three[i].size()) {
+ ++i;
+ j = 0;
+ }
+
+ if ((i >= 3) or (c != three[i][j])) {
+ good = false;
+ }
+
+ ++j;
+
+ return *this;
+ }
+
+ bool
+ error() const override {
+ return false;
+ }
+
+ // Dummies.
+ const char *
+ data() const override {
+ return nullptr;
+ }
+ size_t
+ capacity() const override {
+ return 0;
+ }
+ size_t
+ extent() const override {
+ return 0;
+ }
+ X &restrict(size_t) override { return *this; }
+ X &
+ restore(size_t) override {
+ return *this;
+ }
+ bool
+ commit(size_t) override {
+ return true;
+ }
+ X &
+ discard(size_t) override {
+ return *this;
+ }
+ X &
+ copy(size_t, size_t, size_t) override {
+ return *this;
+ }
+ std::ostream &
+ operator>>(std::ostream &stream) const override {
+ return stream;
+ }
+ };
+
+ X x;
+
+ static_cast<swoc::BufferWriter &>(x).write(three[0]).write(three[1]).write(three[2]);
+
+ REQUIRE(x.good);
+}
+
+namespace {
+template <size_t N> using LBW = swoc::LocalBufferWriter<N>;
+}
+
+TEST_CASE("Minimal Local Buffer Writer", "[BWLM]") {
+ LBW<1> bw;
+
+ REQUIRE(!((bw.capacity() != 1) or (bw.size() != 0) or bw.error() or (bw.remaining() != 1)));
+
+ bw.write('#');
+
+ REQUIRE(!((bw.capacity() != 1) or (bw.size() != 1) or bw.error() or (bw.remaining() != 0)));
+
+ REQUIRE(bw.view() == "#");
+
+ bw.write('!');
+
+ REQUIRE(bw.error());
+
+ bw.discard(1);
+
+ REQUIRE(!((bw.capacity() != 1) or (bw.size() != 1) or bw.error() or (bw.remaining() != 0)));
+
+ REQUIRE(bw.view() == "#");
+}
+
+namespace {
+template <class BWType>
+bool
+twice(BWType &bw) {
+ if ((bw.capacity() != 20) or (bw.size() != 0) or bw.error() or (bw.remaining() != 20)) {
+ return false;
+ }
+
+ bw.write('T');
+
+ if ((bw.capacity() != 20) or (bw.size() != 1) or bw.error() or (bw.remaining() != 19)) {
+ return false;
+ }
+
+ if (bw.view() != "T") {
+ return false;
+ }
+
+ bw.write("he").write(' ').write("quick").write(' ').write("brown");
+
+ if ((bw.capacity() != 20) or bw.error() or (bw.remaining() != (21 - sizeof("The quick brown")))) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown") {
+ return false;
+ }
+
+ bw.clear();
+
+ bw << "The" << ' ' << "quick" << ' ' << "brown";
+
+ if ((bw.capacity() != 20) or bw.error() or (bw.remaining() != (21 - sizeof("The quick brown")))) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown") {
+ return false;
+ }
+
+ bw.clear();
+
+ bw.write("The", 3).write(' ').write("quick", 5).write(' ').write(std::string_view("brown", 5));
+
+ if ((bw.capacity() != 20) or bw.error() or (bw.remaining() != (21 - sizeof("The quick brown")))) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown") {
+ return false;
+ }
+
+ std::strcpy(bw.aux_buffer(), " fox");
+ bw.commit(sizeof(" fox") - 1);
+
+ if (bw.error()) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown fox") {
+ return false;
+ }
+
+ bw.write('x');
+
+ if (bw.error()) {
+ return false;
+ }
+
+ bw.write('x');
+
+ if (!bw.error()) {
+ return false;
+ }
+
+ bw.write('x');
+
+ if (!bw.error()) {
+ return false;
+ }
+
+ bw.reduce(0);
+
+ if (bw.error()) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown fox") {
+ return false;
+ }
+
+ bw.reduce(4);
+ bw.discard(bw.capacity() + 2 - (sizeof("The quick brown fox") - 1)).write(" fox");
+
+ if (bw.view() != "The quick brown f") {
+ return false;
+ }
+
+ if (!bw.error()) {
+ return false;
+ }
+
+ bw.restore(2).write("ox");
+
+ if (bw.error()) {
+ return false;
+ }
+
+ if (bw.view() != "The quick brown fox") {
+ return false;
+ }
+
+ return true;
+}
+
+} // end anonymous namespace
+
+TEST_CASE("Discard Buffer Writer", "[BWD]") {
+ char scratch[1] = {'!'};
+ swoc::FixedBufferWriter bw(scratch, 0);
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == 0);
+
+ bw.write('T');
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == 1);
+
+ bw.write("he").write(' ').write("quick").write(' ').write("brown");
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == (sizeof("The quick brown") - 1));
+
+ bw.clear();
+
+ bw.write("The", 3).write(' ').write("quick", 5).write(' ').write(std::string_view("brown", 5));
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == (sizeof("The quick brown") - 1));
+
+ bw.commit(sizeof(" fox") - 1);
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == (sizeof("The quick brown fox") - 1));
+
+ bw.discard(0);
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == (sizeof("The quick brown fox") - 1));
+
+ bw.discard(4);
+
+ REQUIRE(bw.size() == 0);
+ REQUIRE(bw.extent() == (sizeof("The quick brown") - 1));
+
+ // Make sure no actual writing.
+ //
+ REQUIRE(scratch[0] == '!');
+}
+
+TEST_CASE("LocalBufferWriter discard/restore", "[BWD]") {
+ swoc::LocalBufferWriter<10> bw;
+
+ bw.restrict(7);
+ bw.write("aaaaaa");
+ REQUIRE(bw.view() == "aaa");
+
+ bw.restore(3);
+ bw.write("bbbbbb");
+ REQUIRE(bw.view() == "aaabbb");
+
+ bw.restore(4);
+ bw.commit(static_cast<size_t>(snprintf(bw.aux_data(), bw.remaining(), "ccc")));
+ REQUIRE(bw.view() == "aaabbbccc");
+}
+
+TEST_CASE("Writing", "[BW]") {
+ swoc::LocalBufferWriter<1024> bw;
+
+ // Test run length encoding.
+ TextView s1 = "Delain";
+ TextView s2 = "Nightwish";
+ uint8_t const r[] = {
+ uint8_t(s1.size()), 'D', 'e', 'l', 'a', 'i', 'n', uint8_t(s2.size()), 'N', 'i', 'g', 'h', 't', 'w', 'i', 's', 'h'};
+
+ bw.print("{}{}{}{}", char(s1.size()), s1, char(s2.size()), s2);
+ auto result{swoc::MemSpan{bw.view()}.rebind<uint8_t const>()};
+ REQUIRE(result[0] == s1.size());
+ REQUIRE(result[s1.size() + 1] == s2.size());
+ REQUIRE(MemSpan(r) == result);
+}
+
+TEST_CASE("ArenaWriter write", "[BW][ArenaWriter]") {
+ swoc::MemArena arena{256};
+ swoc::ArenaWriter aw{arena};
+ std::array<char, 85> buffer;
+
+ for (char c = 'a'; c <= 'z'; ++c) {
+ memset(buffer.data(), c, buffer.size());
+ aw.write(buffer.data(), buffer.size());
+ }
+
+ auto constexpr N = 26 * buffer.size();
+ REQUIRE(aw.extent() == N);
+ REQUIRE(aw.size() == N);
+ REQUIRE(arena.remaining() >= N);
+
+ // It's all in the remnant, so allocating it shouldn't affect the overall reserved memory.
+ auto k = arena.reserved_size();
+ auto span = arena.alloc(N);
+ REQUIRE(arena.reserved_size() == k);
+ // The allocated data should be identical to that in the writer.
+ REQUIRE(0 == memcmp(span.data(), aw.data(), span.size()));
+
+ bool valid_p = true;
+ auto tv = swoc::TextView(span.rebind<char>());
+ try {
+ for (char c = 'a'; c <= 'z'; ++c) {
+ for (size_t i = 0; i < buffer.size(); ++i) {
+ if (c != *tv++) {
+ throw std::exception{};
+ }
+ }
+ }
+ } catch (std::exception &ex) {
+ valid_p = false;
+ }
+ REQUIRE(valid_p == true);
+}
+
+TEST_CASE("ArenaWriter print", "[BW][ArenaWriter]") {
+ swoc::MemArena arena{256};
+ swoc::ArenaWriter aw{arena};
+ std::array<char, 85> buffer;
+ swoc::TextView view{buffer.data(), buffer.size()};
+
+ for (char c = 'a'; c <= 'z'; ++c) {
+ memset(buffer.data(), c, buffer.size());
+ aw.print("{}{}{}{}{}", view.substr(0, 25), view.substr(25, 15), view.substr(40, 17), view.substr(57, 19), view.substr(76, 9));
+ }
+
+ auto constexpr N = 26 * buffer.size();
+ REQUIRE(aw.extent() == N);
+ REQUIRE(aw.size() == N);
+ REQUIRE(arena.remaining() >= N);
+
+ // It's all in the remnant, so allocating it shouldn't affect the overall reserved memory.
+ auto k = arena.reserved_size();
+ auto span = arena.alloc(N).rebind<char>();
+ REQUIRE(arena.reserved_size() == k);
+ // The allocated data should be identical to that in the writer.
+ REQUIRE(0 == memcmp(span.data(), aw.data(), span.size()));
+
+ bool valid_p = true;
+ auto tv = swoc::TextView(span);
+ try {
+ for (char c = 'a'; c <= 'z'; ++c) {
+ for (size_t i = 0; i < buffer.size(); ++i) {
+ if (c != *tv++) {
+ throw std::exception{};
+ }
+ }
+ }
+ } catch (std::exception &ex) {
+ valid_p = false;
+ }
+ REQUIRE(valid_p == true);
+}
+
+#if 0
+// Need Endpoint or some other IP address parsing support to load the test values.
+TEST_CASE("BufferWriter IP", "[libswoc][ip][bwf]") {
+ IpEndpoint ep;
+ std::string_view addr_1{"[ffee::24c3:3349:3cee:143]:8080"};
+ std::string_view addr_2{"172.17.99.231:23995"};
+ std::string_view addr_3{"[1337:ded:BEEF::]:53874"};
+ std::string_view addr_4{"[1337::ded:BEEF]:53874"};
+ std::string_view addr_5{"[1337:0:0:ded:BEEF:0:0:956]:53874"};
+ std::string_view addr_6{"[1337:0:0:ded:BEEF:0:0:0]:53874"};
+ std::string_view addr_7{"172.19.3.105:4951"};
+ std::string_view addr_null{"[::]:53874"};
+ swoc::LocalBufferWriter<1024> w;
+
+ REQUIRE(0 == ats_ip_pton(addr_1, &ep.sa));
+ w.clear().print("{}", ep);
+ REQUIRE(w.view() == addr_1);
+ w.clear().print("{::p}", ep);
+ REQUIRE(w.view() == "8080");
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == addr_1.substr(1, 24)); // check the brackets are dropped.
+ w.clear().print("[{::a}]", ep);
+ REQUIRE(w.view() == addr_1.substr(0, 26)); // check the brackets are dropped.
+ w.clear().print("[{0::a}]:{0::p}", ep);
+ REQUIRE(w.view() == addr_1); // check the brackets are dropped.
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "ffee:0000:0000:0000:24c3:3349:3cee:0143");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "ffee: 0: 0: 0:24c3:3349:3cee: 143");
+ ep.setToLoopback(AF_INET6);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "::1");
+ REQUIRE(0 == ats_ip_pton(addr_3, &ep.sa));
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337:ded:beef::");
+ REQUIRE(0 == ats_ip_pton(addr_4, &ep.sa));
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337::ded:beef");
+
+ REQUIRE(0 == ats_ip_pton(addr_5, &ep.sa));
+ w.clear().print("{:X:a}", ep);
+ REQUIRE(w.view() == "1337::DED:BEEF:0:0:956");
+
+ REQUIRE(0 == ats_ip_pton(addr_6, &ep.sa));
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337:0:0:ded:beef::");
+
+ REQUIRE(0 == ats_ip_pton(addr_null, &ep.sa));
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "::");
+
+ REQUIRE(0 == ats_ip_pton(addr_2, &ep.sa));
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == addr_2.substr(0, 13));
+ w.clear().print("{0::a}", ep);
+ REQUIRE(w.view() == addr_2.substr(0, 13));
+ w.clear().print("{::ap}", ep);
+ REQUIRE(w.view() == addr_2);
+ w.clear().print("{::f}", ep);
+ REQUIRE(w.view() == IP_PROTO_TAG_IPV4);
+ w.clear().print("{::fpa}", ep);
+ REQUIRE(w.view() == "172.17.99.231:23995 ipv4");
+ w.clear().print("{0::a} .. {0::p}", ep);
+ REQUIRE(w.view() == "172.17.99.231 .. 23995");
+ w.clear().print("<+> {0::a} <+> {0::p}", ep);
+ REQUIRE(w.view() == "<+> 172.17.99.231 <+> 23995");
+ w.clear().print("<+> {0::a} <+> {0::p} <+>", ep);
+ REQUIRE(w.view() == "<+> 172.17.99.231 <+> 23995 <+>");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "172. 17. 99.231");
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "172.017.099.231");
+
+ // Documentation examples
+ REQUIRE(0 == ats_ip_pton(addr_7, &ep.sa));
+ w.clear().print("To {}", ep);
+ REQUIRE(w.view() == "To 172.19.3.105:4951");
+ w.clear().print("To {0::a} on port {0::p}", ep); // no need to pass the argument twice.
+ REQUIRE(w.view() == "To 172.19.3.105 on port 4951");
+ w.clear().print("To {::=}", ep);
+ REQUIRE(w.view() == "To 172.019.003.105:04951");
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "172.19.3.105");
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "172.019.003.105");
+ w.clear().print("{::0=a}", ep);
+ REQUIRE(w.view() == "172.019.003.105");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "172. 19. 3.105");
+ w.clear().print("{:>20:a}", ep);
+ REQUIRE(w.view() == " 172.19.3.105");
+ w.clear().print("{:>20:=a}", ep);
+ REQUIRE(w.view() == " 172.019.003.105");
+ w.clear().print("{:>20: =a}", ep);
+ REQUIRE(w.view() == " 172. 19. 3.105");
+ w.clear().print("{:<20:a}", ep);
+ REQUIRE(w.view() == "172.19.3.105 ");
+
+ w.clear().print("{:p}", reinterpret_cast<sockaddr const *>(0x1337beef));
+ REQUIRE(w.view() == "0x1337beef");
+}
+#endif
diff --git a/lib/swoc/unit_tests/test_Errata.cc b/lib/swoc/unit_tests/test_Errata.cc
new file mode 100644
index 0000000000..ea4e2e8a81
--- /dev/null
+++ b/lib/swoc/unit_tests/test_Errata.cc
@@ -0,0 +1,430 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ Errata unit tests.
+*/
+
+#include <memory>
+#include <errno.h>
+#include "swoc/Errata.h"
+#include "swoc/bwf_std.h"
+#include "swoc/bwf_ex.h"
+#include "swoc/swoc_file.h"
+#include "swoc/Lexicon.h"
+#include "catch.hpp"
+
+using swoc::Errata;
+using swoc::Rv;
+using swoc::TextView;
+using Severity = swoc::Errata::Severity;
+using namespace std::literals;
+using namespace swoc::literals;
+
+static constexpr swoc::Errata::Severity ERRATA_DBG{0};
+static constexpr swoc::Errata::Severity ERRATA_DIAG{1};
+static constexpr swoc::Errata::Severity ERRATA_INFO{2};
+static constexpr swoc::Errata::Severity ERRATA_WARN{3};
+static constexpr swoc::Errata::Severity ERRATA_ERROR{4};
+
+std::array<swoc::TextView, 5> Severity_Names{
+ {"Debug", "Diag", "Info", "Warn", "Error"}
+};
+
+enum class ECode { ALPHA = 1, BRAVO, CHARLIE };
+
+struct e_category : std::error_category {
+ const char *name() const noexcept override;
+ std::string message(int ev) const override;
+};
+
+e_category e_cat;
+
+const char *
+e_category::name() const noexcept {
+ return "libswoc";
+}
+
+std::string
+e_category::message(int ev) const {
+ static swoc::Lexicon<ECode> lexicon{
+ {{ECode::ALPHA, "Alpha"}, {ECode::BRAVO, "Bravo"}, {ECode::CHARLIE, "Charlie"}},
+ "Code out of range"
+ };
+
+ return std::string(lexicon[ECode(ev)]);
+}
+
+inline std::error_code
+ecode(ECode c) {
+ return {int(c), e_cat};
+}
+
+std::string ErrataSinkText;
+
+// Call from unit test main before starting tests.
+void
+test_Errata_init() {
+ swoc::Errata::DEFAULT_SEVERITY = ERRATA_ERROR;
+ swoc::Errata::FAILURE_SEVERITY = ERRATA_WARN;
+ swoc::Errata::SEVERITY_NAMES = swoc::MemSpan<swoc::TextView const>(Severity_Names.data(), Severity_Names.size());
+
+ swoc::Errata::register_sink([](swoc::Errata const &errata) -> void { bwprint(ErrataSinkText, "{}", errata); });
+}
+
+Errata
+Noteworthy(std::string_view text) {
+ return Errata{ERRATA_INFO, text};
+}
+
+Errata
+cycle(Errata &erratum) {
+ return std::move(erratum.note("Note well, young one!"));
+}
+
+TEST_CASE("Errata copy", "[libswoc][Errata]") {
+ auto notes = Noteworthy("Evil Dave Rulz.");
+ REQUIRE(notes.length() == 1);
+ REQUIRE(notes.begin()->text() == "Evil Dave Rulz.");
+
+ notes = cycle(notes);
+ REQUIRE(notes.length() == 2);
+
+ Errata erratum;
+ REQUIRE(erratum.length() == 0);
+ erratum.note("Diagnostics");
+ REQUIRE(erratum.length() == 1);
+ erratum.note("Information");
+ REQUIRE(erratum.length() == 2);
+
+ // Test internal allocation boundaries.
+ notes.clear();
+ std::string_view text{"0123456789012345678901234567890123456789"};
+ for (int i = 0; i < 50; ++i) {
+ notes.note(text);
+ }
+ REQUIRE(notes.length() == 50);
+ REQUIRE(notes.begin()->text() == text);
+ bool match_p = true;
+ for (auto &¬e : notes) {
+ if (note.text() != text) {
+ match_p = false;
+ break;
+ }
+ }
+ REQUIRE(match_p);
+};
+
+TEST_CASE("Rv", "[libswoc][Errata]") {
+ Rv<int> zret;
+ struct Thing {
+ char const *s = "thing";
+ };
+ using ThingHandle = std::unique_ptr<Thing>;
+
+ zret = 17;
+ zret = Errata(std::error_code(EINVAL, std::generic_category()), ERRATA_ERROR, "This is an error");
+
+ {
+ auto &[result, erratum] = zret;
+
+ REQUIRE(erratum.length() == 1);
+ REQUIRE(erratum.severity() == ERRATA_ERROR);
+ REQUIRE_FALSE(erratum.is_ok());
+
+ REQUIRE(result == 17);
+ zret = 38;
+ REQUIRE(result == 38); // reference binding, update.
+ }
+
+ {
+ auto &&[result, erratum] = zret;
+
+ REQUIRE(erratum.length() == 1);
+ REQUIRE(erratum.severity() == ERRATA_ERROR);
+
+ REQUIRE(result == 38);
+ zret = 56;
+ REQUIRE(result == 56); // reference binding, update.
+ }
+
+ auto test = [](Severity expected_severity, Rv<int> const &rvc) {
+ auto const &[cv_result, cv_erratum] = rvc;
+ REQUIRE(cv_erratum.length() == 1);
+ REQUIRE(cv_erratum.severity() == expected_severity);
+ REQUIRE(cv_result == 56);
+ };
+
+ {
+ auto const &[result, erratum] = zret;
+ REQUIRE(result == 56);
+
+ test(ERRATA_ERROR, zret); // invoke it.
+ }
+
+ zret.clear();
+ REQUIRE(zret.result() == 56);
+
+ {
+ auto const &[result, erratum] = zret;
+ REQUIRE(result == 56);
+ REQUIRE(erratum.length() == 0);
+ }
+
+ zret.note("Diagnostics");
+ REQUIRE(zret.errata().length() == 1);
+ zret.note("Information");
+ REQUIRE(zret.errata().length() == 2);
+ zret.note("Warning");
+ REQUIRE(zret.errata().length() == 3);
+ zret.note("Error");
+ REQUIRE(zret.errata().length() == 4);
+ REQUIRE(zret.result() == 56);
+
+ test(ERRATA_DIAG, Rv<int>{56, Errata(ERRATA_DIAG, "Test rvalue diag")});
+ test(ERRATA_INFO, Rv<int>{56, Errata(ERRATA_INFO, "Test rvalue info")});
+ test(ERRATA_WARN, Rv<int>{56, Errata(ERRATA_WARN, "Test rvalue warn")});
+ test(ERRATA_ERROR, Rv<int>{56, Errata(ERRATA_ERROR, "Test rvalue error")});
+
+ // Test the note overload that takes an Errata.
+ zret.clear();
+ REQUIRE(zret.result() == 56);
+ REQUIRE(zret.errata().length() == 0);
+ zret = Errata{ERRATA_INFO, "Information"};
+ REQUIRE(ERRATA_INFO == zret.errata().severity());
+ REQUIRE(zret.errata().length() == 1);
+
+ Errata e1{ERRATA_DBG, "Debug"};
+ zret.note(e1);
+ REQUIRE(zret.errata().length() == 2);
+ REQUIRE(ERRATA_INFO == zret.errata().severity());
+
+ Errata e2{ERRATA_DBG, "Debug"};
+ zret.note(std::move(e2));
+ REQUIRE(zret.errata().length() == 3);
+ REQUIRE(e2.length() == 0);
+
+ // Now try it on a non-copyable object.
+ ThingHandle handle{new Thing};
+ Rv<ThingHandle> thing_rv;
+
+ handle->s = "other"; // mark it.
+ thing_rv = std::move(handle);
+ thing_rv = Errata(ERRATA_WARN, "This is a warning");
+
+ auto &&[tr1, te1]{thing_rv};
+ REQUIRE(te1.length() == 1);
+ REQUIRE(te1.severity() == ERRATA_WARN);
+ REQUIRE_FALSE(te1.is_ok());
+
+ ThingHandle other{std::move(tr1)};
+ REQUIRE(tr1.get() == nullptr);
+ REQUIRE(thing_rv.result().get() == nullptr);
+ REQUIRE(other->s == "other"sv);
+
+ auto maker = []() -> Rv<ThingHandle> {
+ ThingHandle handle = std::make_unique<Thing>();
+ handle->s = "made";
+ return {std::move(handle)};
+ };
+
+ auto &&[tr2, te2]{maker()};
+ REQUIRE(tr2->s == "made"sv);
+};
+
+// DOC -> NoteInfo
+template <typename... Args>
+Errata &
+NoteInfo(Errata &errata, std::string_view fmt, Args... args) {
+ return errata.note_v(ERRATA_INFO, fmt, std::forward_as_tuple(args...));
+}
+// DOC -< NoteInfo
+
+TEST_CASE("Errata example", "[libswoc][Errata]") {
+ swoc::LocalBufferWriter<2048> w;
+ std::error_code ec;
+ swoc::file::path path("does-not-exist.txt");
+ auto content = swoc::file::load(path, ec);
+ REQUIRE(false == !ec); // it is expected the load will fail.
+ Errata errata{ec, ERRATA_ERROR, R"(Failed to open file "{}")", path};
+ w.print("{}", errata);
+ REQUIRE(w.size() > 0);
+ REQUIRE(w.view().starts_with("Error: [enoent") == true);
+ REQUIRE(w.view().find("enoent") != swoc::TextView::npos);
+}
+
+TEST_CASE("Errata API", "[libswoc][Errata]") {
+ // Check that if an int is expected from a function, it can be changed to
+ // @c Rv<int> without change at the call site.
+ int size = -7;
+ auto f = [&]() -> Rv<int> {
+ if (size > 0)
+ return size;
+ return {-1, Errata(ERRATA_ERROR, "No size, doofus!")};
+ };
+
+ int r1 = f();
+ REQUIRE(r1 == -1);
+ size = 10;
+ int r2 = f();
+ REQUIRE(r2 == 10);
+}
+
+TEST_CASE("Errata sink", "[libswoc][Errata]") {
+ auto &s = ErrataSinkText;
+ {
+ Errata errata{ERRATA_ERROR, "Nominal failure"};
+ NoteInfo(errata, "Some");
+ errata.note(ERRATA_DIAG, "error code {}", std::error_code(EPERM, std::system_category()));
+ }
+ // Destruction should write this out to the string.
+ REQUIRE(s.size() > 0);
+ REQUIRE(std::string::npos != s.find("Error: Nominal"));
+ REQUIRE(std::string::npos != s.find("Info: Some"));
+ REQUIRE(std::string::npos != s.find("Diag: error"));
+
+ {
+ Errata errata{ERRATA_ERROR, "Nominal failure"};
+ NoteInfo(errata, "Some");
+ errata.note(ERRATA_DIAG, "error code {}", std::error_code(EPERM, std::system_category()));
+ errata.sink();
+
+ REQUIRE(s.size() > 0);
+ REQUIRE(std::string::npos != s.find("Error: Nominal"));
+ REQUIRE(std::string::npos != s.find("Info: Some"));
+ REQUIRE(std::string::npos != s.find("Diag: error"));
+
+ s.clear();
+ }
+
+ REQUIRE(s.empty() == true);
+ {
+ Errata errata{ERRATA_ERROR, "Nominal failure"};
+ NoteInfo(errata, "Some");
+ errata.note(ERRATA_DIAG, "error code {}", std::error_code(EPERM, std::system_category()));
+ errata.clear(); // cleared - no logging
+ REQUIRE(errata.is_ok() == true);
+ }
+ REQUIRE(s.empty() == true);
+}
+
+TEST_CASE("Errata local severity", "[libswoc][Errata]") {
+ std::string s;
+ {
+ Errata errata{ERRATA_ERROR, "Nominal failure"};
+ NoteInfo(errata, "Some");
+ errata.note(ERRATA_DIAG, "error code {}", std::error_code(EPERM, std::system_category()));
+ swoc::bwprint(s, "{}", errata);
+ REQUIRE(s.size() > 0);
+ REQUIRE(std::string::npos != s.find("Error: Nominal"));
+ REQUIRE(std::string::npos != s.find("Info: Some"));
+ REQUIRE(std::string::npos != s.find("Diag: error"));
+ }
+ Errata::FILTER_SEVERITY = ERRATA_INFO; // diag is lesser serverity, shouldn't show up.
+ {
+ Errata errata{ERRATA_ERROR, "Nominal failure"};
+ NoteInfo(errata, "Some");
+ errata.note(ERRATA_DIAG, "error code {}", std::error_code(EPERM, std::system_category()));
+ swoc::bwprint(s, "{}", errata);
+ REQUIRE(s.size() > 0);
+ REQUIRE(std::string::npos != s.find("Error: Nominal"));
+ REQUIRE(std::string::npos != s.find("Info: Some"));
+ REQUIRE(std::string::npos == s.find("Diag: error"));
+ }
+
+ Errata base{ERRATA_INFO, "Something happened"};
+ base.note(Errata{ERRATA_WARN}.note(ERRATA_INFO, "Thing one").note(ERRATA_INFO, "Thing Two"));
+ REQUIRE(base.length() == 3);
+ REQUIRE(base.severity() == ERRATA_WARN);
+}
+
+TEST_CASE("Errata glue", "[libswoc][Errata]") {
+ std::string s;
+ Errata errata;
+
+ errata.note(ERRATA_ERROR, "First");
+ errata.note(ERRATA_WARN, "Second");
+ errata.note(ERRATA_INFO, "Third");
+ errata.assign_severity_glue_text(":\n");
+ bwprint(s, "{}", errata);
+ REQUIRE("Error:\nError: First\nWarn: Second\nInfo: Third\n" == s);
+ errata.assign_annotation_glue_text("\n"); // check for no trailing newline
+ bwprint(s, "{}", errata);
+ REQUIRE("Error:\nError: First\nWarn: Second\nInfo: Third" == s);
+ errata.assign_annotation_glue_text("\n", true); // check for trailing newline
+ bwprint(s, "{}", errata);
+ REQUIRE("Error:\nError: First\nWarn: Second\nInfo: Third\n" == s);
+
+ errata.assign_annotation_glue_text(", ");
+ bwprint(s, "{}", errata);
+ REQUIRE("Error:\nError: First, Warn: Second, Info: Third" == s);
+
+ errata.clear();
+ errata.note("First");
+ errata.note("Second");
+ errata.note("Third");
+ errata.assign(ERRATA_ERROR);
+ errata.assign_severity_glue_text(" -> ");
+ errata.assign_annotation_glue_text(", ");
+ bwprint(s, "{}", errata);
+ REQUIRE("Error -> First, Second, Third" == s);
+}
+
+template <typename... Args>
+Errata
+errata_errno(int err, Errata::Severity s, swoc::TextView fmt, Args &&...args) {
+ return Errata(std::error_code(err, std::system_category()), s, "{} - {}",
+ swoc::bwf::SubText(fmt, std::forward_as_tuple<Args...>(args...)), swoc::bwf::Errno(err));
+}
+
+template <typename... Args>
+Errata
+errata_errno(Errata::Severity s, swoc::TextView fmt, Args &&...args) {
+ return errata_errno(errno, s, fmt, std::forward<Args>(args)...);
+}
+
+TEST_CASE("Errata Wrapper", "[libswoc][errata]") {
+ TextView tv1 = "itchi";
+ TextView tv2 = "ni";
+
+ SECTION("no args") {
+ errno = EPERM;
+ auto errata = errata_errno(ERRATA_ERROR, "no args");
+ REQUIRE(errata.front().text().starts_with("no args - EPERM"));
+ }
+
+ SECTION("one arg, explcit") {
+ auto errata = errata_errno(EPERM, ERRATA_ERROR, "no args");
+ REQUIRE(errata.front().text().starts_with("no args - EPERM"));
+ }
+
+ SECTION("args, explcit") {
+ auto errata = errata_errno(EBADF, ERRATA_ERROR, "{} {}", tv1, tv2);
+ REQUIRE(errata.front().text().starts_with("itchi ni - EBADF"));
+ }
+
+ SECTION("args") {
+ errno = EINVAL;
+ auto errata = errata_errno(ERRATA_ERROR, "{} {}", tv2, tv1);
+ REQUIRE(errata.front().text().starts_with("ni itchi - EINVAL"));
+ }
+}
+
+TEST_CASE("Errata Autotext", "[libswoc][errata]") {
+ Errata a{ERRATA_WARN, Errata::AUTO};
+ REQUIRE(a.front().text() == "Warn");
+ Errata b{ecode(ECode::BRAVO), Errata::AUTO};
+ REQUIRE(b.front().text() == "Bravo [2]");
+ Errata c{ecode(ECode::ALPHA), ERRATA_ERROR, Errata::AUTO};
+ REQUIRE(c.front().text() == "Error: Alpha [1]");
+
+ Errata d{ERRATA_ERROR};
+ REQUIRE_FALSE(d.is_ok());
+ Errata e{ERRATA_INFO};
+ REQUIRE(e.is_ok());
+ Errata f{ecode(ECode::BRAVO)};
+ REQUIRE_FALSE(f.is_ok());
+ // Change properties but need to restore them for other tests.
+ swoc::meta::let g1(Errata::DEFAULT_SEVERITY, ERRATA_WARN);
+ swoc::meta::let g2(Errata::FAILURE_SEVERITY, ERRATA_ERROR);
+ REQUIRE(f.is_ok());
+}
diff --git a/lib/swoc/unit_tests/test_IntrusiveDList.cc b/lib/swoc/unit_tests/test_IntrusiveDList.cc
new file mode 100644
index 0000000000..51f57a09e8
--- /dev/null
+++ b/lib/swoc/unit_tests/test_IntrusiveDList.cc
@@ -0,0 +1,272 @@
+/** @file
+
+ IntrusiveDList unit tests.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <iostream>
+#include <string_view>
+#include <string>
+#include <algorithm>
+
+#include "swoc/IntrusiveDList.h"
+#include "swoc/bwf_base.h"
+
+#include "catch.hpp"
+
+using swoc::IntrusiveDList;
+using swoc::bwprint;
+
+namespace {
+struct Thing {
+ std::string _payload;
+ Thing *_next{nullptr};
+ Thing *_prev{nullptr};
+
+ Thing(std::string_view text) : _payload(text) {}
+
+ struct Linkage {
+ static Thing *&
+ next_ptr(Thing *t) {
+ return t->_next;
+ }
+
+ static Thing *&
+ prev_ptr(Thing *t) {
+ return t->_prev;
+ }
+ };
+};
+
+using ThingList = IntrusiveDList<Thing::Linkage>;
+
+} // namespace
+
+TEST_CASE("IntrusiveDList", "[libswoc][IntrusiveDList]") {
+ ThingList list;
+ int n;
+
+ REQUIRE(list.count() == 0);
+ REQUIRE(list.head() == nullptr);
+ REQUIRE(list.tail() == nullptr);
+ REQUIRE(list.begin() == list.end());
+ REQUIRE(list.empty());
+
+ n = 0;
+ for ([[maybe_unused]] auto &thing : list)
+ ++n;
+ REQUIRE(n == 0);
+ // Check const iteration (mostly compile checks here).
+ for ([[maybe_unused]] auto &thing : static_cast<ThingList const &>(list))
+ ++n;
+ REQUIRE(n == 0);
+
+ list.append(new Thing("one"));
+ REQUIRE(list.begin() != list.end());
+ REQUIRE(list.tail() == list.head());
+
+ list.prepend(new Thing("two"));
+ REQUIRE(list.count() == 2);
+ REQUIRE(list.head()->_payload == "two");
+ REQUIRE(list.tail()->_payload == "one");
+ list.prepend(list.take_tail());
+ REQUIRE(list.head()->_payload == "one");
+ REQUIRE(list.tail()->_payload == "two");
+ list.insert_after(list.head(), new Thing("middle"));
+ list.insert_before(list.tail(), new Thing("muddle"));
+ REQUIRE(list.count() == 4);
+ auto spot = list.begin();
+ REQUIRE((*spot++)._payload == "one");
+ REQUIRE((*spot++)._payload == "middle");
+ REQUIRE((*spot++)._payload == "muddle");
+ REQUIRE((*spot++)._payload == "two");
+ REQUIRE(spot == list.end());
+ spot = list.begin(); // verify assignment works.
+
+ Thing *thing = list.take_head();
+ REQUIRE(thing->_payload == "one");
+ REQUIRE(list.count() == 3);
+ REQUIRE(list.head() != nullptr);
+ REQUIRE(list.head()->_payload == "middle");
+
+ list.prepend(thing);
+ list.erase(list.head());
+ REQUIRE(list.count() == 3);
+ REQUIRE(list.head() != nullptr);
+ REQUIRE(list.head()->_payload == "middle");
+ list.prepend(thing);
+
+ thing = list.take_tail();
+ REQUIRE(thing->_payload == "two");
+ REQUIRE(list.count() == 3);
+ REQUIRE(list.tail() != nullptr);
+ REQUIRE(list.tail()->_payload == "muddle");
+
+ list.append(thing);
+ list.erase(list.tail());
+ REQUIRE(list.count() == 3);
+ REQUIRE(list.tail() != nullptr);
+ REQUIRE(list.tail()->_payload == "muddle");
+ REQUIRE(list.head()->_payload == "one");
+
+ list.insert_before(list.end(), new Thing("trailer"));
+ REQUIRE(list.count() == 4);
+ REQUIRE(list.tail()->_payload == "trailer");
+}
+
+TEST_CASE("IntrusiveDList list prefix", "[libswoc][IntrusiveDList]") {
+ ThingList list;
+
+ std::string tmp;
+ for (unsigned idx = 1; idx <= 20; ++idx) {
+ list.append(new Thing(bwprint(tmp, "{}", idx)));
+ }
+
+ auto x = list.nth(0);
+ REQUIRE(x->_payload == "1");
+ x = list.nth(19);
+ REQUIRE(x->_payload == "20");
+
+ auto list_none = list.take_prefix(0);
+ REQUIRE(list_none.count() == 0);
+ REQUIRE(list_none.head() == nullptr);
+ REQUIRE(list.count() == 20);
+
+ auto v = list.head();
+ auto list_1 = list.take_prefix(1);
+ REQUIRE(list_1.count() == 1);
+ REQUIRE(list_1.head() == v);
+ REQUIRE(list.count() == 19);
+
+ v = list.head();
+ auto list_5 = list.take_prefix(5);
+ REQUIRE(list_5.count() == 5);
+ REQUIRE(list_5.head() == v);
+ REQUIRE(list.count() == 14);
+ REQUIRE(list.head() != nullptr);
+ REQUIRE(list.head()->_payload == "7");
+
+ v = list.head();
+ auto list_most = list.take_prefix(9); // more than half.
+ REQUIRE(list_most.count() == 9);
+ REQUIRE(list_most.head() == v);
+ REQUIRE(list.count() == 5);
+ REQUIRE(list.head() != nullptr);
+
+ v = list.head();
+ auto list_rest = list.take_prefix(20);
+ REQUIRE(list_rest.count() == 5);
+ REQUIRE(list_rest.head() == v);
+ REQUIRE(list_rest.head()->_payload == "16");
+ REQUIRE(list.count() == 0);
+ REQUIRE(list.head() == nullptr);
+}
+
+TEST_CASE("IntrusiveDList list suffix", "[libswoc][IntrusiveDList]") {
+ ThingList list;
+
+ std::string tmp;
+ for (unsigned idx = 1; idx <= 20; ++idx) {
+ list.append(new Thing(bwprint(tmp, "{}", idx)));
+ }
+
+ auto list_none = list.take_suffix(0);
+ REQUIRE(list_none.count() == 0);
+ REQUIRE(list_none.head() == nullptr);
+ REQUIRE(list.count() == 20);
+
+ auto *v = list.tail();
+ auto list_1 = list.take_suffix(1);
+ REQUIRE(list_1.count() == 1);
+ REQUIRE(list_1.tail() == v);
+ REQUIRE(list.count() == 19);
+
+ v = list.tail();
+ auto list_5 = list.take_suffix(5);
+ REQUIRE(list_5.count() == 5);
+ REQUIRE(list_5.tail() == v);
+ REQUIRE(list.count() == 14);
+ REQUIRE(list.head() != nullptr);
+ REQUIRE(list.tail()->_payload == "14");
+
+ v = list.tail();
+ auto list_most = list.take_suffix(9); // more than half.
+ REQUIRE(list_most.count() == 9);
+ REQUIRE(list_most.tail() == v);
+ REQUIRE(list.count() == 5);
+ REQUIRE(list.tail() != nullptr);
+
+ v = list.head();
+ auto list_rest = list.take_suffix(20);
+ REQUIRE(list_rest.count() == 5);
+ REQUIRE(list_rest.head() == v);
+ REQUIRE(list_rest.head()->_payload == "1");
+ REQUIRE(list_rest.tail()->_payload == "5");
+ REQUIRE(list.count() == 0);
+ REQUIRE(list.tail() == nullptr);
+
+ // reassemble the list.
+ list.append(list_most); // middle 6..14
+ list_1.prepend(list_5); // -> last 15..20
+ list.prepend(list_rest); // initial, 1..5 -> 1..14
+ list.append(list_1);
+
+ REQUIRE(list.count() == 20);
+ REQUIRE(list.head()->_payload == "1");
+ REQUIRE(list.tail()->_payload == "20");
+ REQUIRE(list.nth(7)->_payload == "8");
+ REQUIRE(list.nth(17)->_payload == "18");
+}
+
+TEST_CASE("IntrusiveDList Extra", "[libswoc][IntrusiveDList]") {
+ struct S {
+ std::string name;
+ swoc::IntrusiveLinks<S> _links;
+ };
+
+ using S_List = swoc::IntrusiveDList<swoc::IntrusiveLinkDescriptor<S, &S::_links>>;
+ [[maybe_unused]] S_List s_list;
+
+ ThingList list, list_b, list_a;
+
+ std::string tmp;
+ list.append(new Thing(bwprint(tmp, "{}", 0)));
+ list.append(new Thing(bwprint(tmp, "{}", 1)));
+ list.append(new Thing(bwprint(tmp, "{}", 2)));
+ list.append(new Thing(bwprint(tmp, "{}", 6)));
+ list.append(new Thing(bwprint(tmp, "{}", 11)));
+ list.append(new Thing(bwprint(tmp, "{}", 12)));
+
+ for (unsigned idx = 3; idx <= 5; ++idx) {
+ list_b.append(new Thing(bwprint(tmp, "{}", idx)));
+ }
+ for (unsigned idx = 7; idx <= 10; ++idx) {
+ list_a.append(new Thing(bwprint(tmp, "{}", idx)));
+ }
+
+ auto v = list.nth(3);
+ REQUIRE(v->_payload == "6");
+
+ list.insert_before(v, list_b);
+ list.insert_after(v, list_a);
+
+ auto spot = list.begin();
+ for (unsigned idx = 0; idx <= 12; ++idx, ++spot) {
+ bwprint(tmp, "{}", idx);
+ REQUIRE(spot->_payload == tmp);
+ }
+}
diff --git a/lib/swoc/unit_tests/test_IntrusiveHashMap.cc b/lib/swoc/unit_tests/test_IntrusiveHashMap.cc
new file mode 100644
index 0000000000..aa228ba8e0
--- /dev/null
+++ b/lib/swoc/unit_tests/test_IntrusiveHashMap.cc
@@ -0,0 +1,274 @@
+/** @file
+
+ IntrusiveHashMap unit tests.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <iostream>
+#include <iterator>
+#include <string_view>
+#include <string>
+#include <bitset>
+#include <random>
+
+#include "swoc/IntrusiveHashMap.h"
+#include "swoc/bwf_base.h"
+#include "catch.hpp"
+
+using swoc::IntrusiveHashMap;
+
+// -------------
+// --- TESTS ---
+// -------------
+
+using namespace std::literals;
+
+namespace {
+struct Thing {
+ std::string _payload;
+ int _n{0};
+
+ Thing(std::string_view text) : _payload(text) {}
+ Thing(std::string_view text, int x) : _payload(text), _n(x) {}
+
+ Thing *_next{nullptr};
+ Thing *_prev{nullptr};
+};
+
+struct ThingMapDescriptor {
+ static Thing *&
+ next_ptr(Thing *thing) {
+ return thing->_next;
+ }
+ static Thing *&
+ prev_ptr(Thing *thing) {
+ return thing->_prev;
+ }
+ static std::string_view
+ key_of(Thing *thing) {
+ return thing->_payload;
+ }
+ static constexpr std::hash<std::string_view> hasher{};
+ static auto
+ hash_of(std::string_view s) -> decltype(hasher(s)) {
+ return hasher(s);
+ }
+ static bool
+ equal(std::string_view const &lhs, std::string_view const &rhs) {
+ return lhs == rhs;
+ }
+};
+
+using Map = IntrusiveHashMap<ThingMapDescriptor>;
+
+} // namespace
+
+TEST_CASE("IntrusiveHashMap", "[libts][IntrusiveHashMap]") {
+ Map map;
+ map.insert(new Thing("bob"));
+ REQUIRE(map.count() == 1);
+ map.insert(new Thing("dave"));
+ map.insert(new Thing("persia"));
+ REQUIRE(map.count() == 3);
+ // Need to be bit careful cleaning up, since the link pointers are in the objects and deleting
+ // the object makes it unsafe to use an iterator referencing that object. For a full cleanup,
+ // the best option is to first delete everything, then clean up the map.
+ map.apply([](Thing *thing) { delete thing; });
+ map.clear();
+ REQUIRE(map.count() == 0);
+
+ size_t nb = map.bucket_count();
+ std::bitset<64> marks;
+ for (size_t i = 1; i <= 63; ++i) {
+ std::string name;
+ swoc::bwprint(name, "{} squared is {}", i, i * i);
+ Thing *thing = new Thing(name);
+ thing->_n = i;
+ map.insert(thing);
+ REQUIRE(map.count() == i);
+ REQUIRE(map.find(name) != map.end());
+ }
+ REQUIRE(map.count() == 63);
+ REQUIRE(map.bucket_count() > nb);
+ for (auto &thing : map) {
+ REQUIRE(0 == marks[thing._n]);
+ marks[thing._n] = 1;
+ }
+ marks[0] = 1;
+ REQUIRE(marks.all());
+ map.insert(new Thing("dup"sv, 79));
+
+ // Test equal_range with a single value.
+ auto r = map.equal_range("dup"sv);
+ auto reverse_it = std::make_reverse_iterator(r.end());
+ auto end = std::make_reverse_iterator(r.begin());
+ REQUIRE(reverse_it != end);
+ REQUIRE(reverse_it->_payload == "dup"sv);
+ REQUIRE(reverse_it->_n == 79);
+ REQUIRE((++reverse_it) == end);
+
+ // Add more values for equal_range to interact with.
+ map.insert(new Thing("dup"sv, 80));
+ map.insert(new Thing("dup"sv, 81));
+
+ r = map.equal_range("dup"sv);
+ REQUIRE(r.begin()->_payload == "dup"sv);
+ REQUIRE(r.begin()->_n == 79);
+ REQUIRE(r.first != r.second);
+ REQUIRE(r.first == r.begin());
+ REQUIRE(r.second == r.end());
+
+ // Verify the range is correct and that accessing them one at a time works in
+ // FIFO order.
+ auto iter = r.begin();
+ REQUIRE(iter->_payload == "dup"sv);
+ REQUIRE(iter->_n == 79);
+ REQUIRE((++iter)->_payload == "dup"sv);
+ REQUIRE(iter->_n == 80);
+ REQUIRE((++iter)->_payload == "dup"sv);
+ REQUIRE(iter->_n == 81);
+ REQUIRE((++iter) == r.end());
+
+ // Verify you can walk backwards, accessing the elements in a LIFO order.
+ reverse_it = std::make_reverse_iterator(r.end());
+ end = std::make_reverse_iterator(r.begin());
+ REQUIRE(reverse_it->_payload == "dup"sv);
+ REQUIRE(reverse_it->_n == 81);
+ REQUIRE((++reverse_it)->_payload == "dup"sv);
+ REQUIRE(reverse_it->_n == 80);
+ REQUIRE((++reverse_it)->_payload == "dup"sv);
+ REQUIRE(reverse_it->_n == 79);
+ REQUIRE((++reverse_it) == end);
+
+ Map::iterator idx;
+
+ // Erase all the non-"dup" and see if the range is still correct.
+ map.apply([&map](Thing &thing) {
+ if (thing._payload != "dup"sv)
+ map.erase(map.iterator_for(&thing));
+ });
+ r = map.equal_range("dup"sv);
+ REQUIRE(r.first != r.second);
+ idx = r.first;
+ REQUIRE(idx->_payload == "dup"sv);
+ REQUIRE(idx->_n == 79);
+ REQUIRE((++idx)->_payload == "dup"sv);
+ REQUIRE(idx->_n != r.first->_n);
+ REQUIRE(idx->_n == 80);
+ REQUIRE((++idx)->_payload == "dup"sv);
+ REQUIRE(idx->_n != r.first->_n);
+ REQUIRE(idx->_n == 81);
+ REQUIRE(++idx == map.end());
+
+ // Now verify we can go backwards.
+ REQUIRE((--idx)->_payload == "dup"sv);
+ REQUIRE(idx->_n != r.first->_n);
+ REQUIRE(idx->_n == 81);
+ REQUIRE((--idx)->_payload == "dup"sv);
+ REQUIRE(idx->_n != r.first->_n);
+ REQUIRE(idx->_n == 80);
+ // Verify only the "dup" items are left.
+ for (auto &&elt : map) {
+ REQUIRE(elt._payload == "dup"sv);
+ }
+ // clean up the last bits.
+ map.apply([](Thing *thing) { delete thing; });
+};
+
+// Some more involved tests.
+TEST_CASE("IntrusiveHashMapManyStrings", "[IntrusiveHashMap]") {
+ std::vector<std::string_view> strings;
+
+ std::uniform_int_distribution<short> char_gen{'a', 'z'};
+ std::uniform_int_distribution<short> length_gen{20, 40};
+ std::minstd_rand randu;
+ constexpr int N = 1009;
+
+ Map ihm;
+
+ strings.reserve(N);
+ for (int i = 0; i < N; ++i) {
+ auto len = length_gen(randu);
+ char *s = static_cast<char *>(malloc(len + 1));
+ for (decltype(len) j = 0; j < len; ++j) {
+ s[j] = char_gen(randu);
+ }
+ s[len] = 0;
+ strings.push_back({s, size_t(len)});
+ }
+
+ // Fill the IntrusiveHashMap
+ for (int i = 0; i < N; ++i) {
+ ihm.insert(new Thing{strings[i], i});
+ }
+
+ REQUIRE(ihm.count() == N);
+
+ // Do some lookups - just require the whole loop, don't artificially inflate the test count.
+ bool miss_p = false;
+ for (int j = 0, idx = 17; j < N; ++j, idx = (idx + 17) % N) {
+ if (auto spot = ihm.find(strings[idx]); spot == ihm.end() || spot->_n != idx) {
+ miss_p = true;
+ }
+ }
+ REQUIRE(miss_p == false);
+
+ // Let's try some duplicates when there's a lot of data in the map.
+ miss_p = false;
+ for (int idx = 23; idx < N; idx += 23) {
+ ihm.insert(new Thing(strings[idx], 2000 + idx));
+ }
+ for (int idx = 23; idx < N; idx += 23) {
+ auto spot = ihm.find(strings[idx]);
+ if (spot == ihm.end() || spot->_n != idx || ++spot == ihm.end() || spot->_n != 2000 + idx) {
+ miss_p = true;
+ }
+ }
+ REQUIRE(miss_p == false);
+
+ // Do a different stepping, special cases the intersection with the previous stepping.
+ miss_p = false;
+ for (int idx = 31; idx < N; idx += 31) {
+ ihm.insert(new Thing(strings[idx], 3000 + idx));
+ }
+ for (int idx = 31; idx < N; idx += 31) {
+ auto spot = ihm.find(strings[idx]);
+ if (spot == ihm.end() || spot->_n != idx || ++spot == ihm.end() || (idx != (23 * 31) && spot->_n != 3000 + idx) ||
+ (idx == (23 * 31) && spot->_n != 2000 + idx)) {
+ miss_p = true;
+ }
+ }
+ REQUIRE(miss_p == false);
+
+ // Check for misses.
+ miss_p = false;
+ for (int i = 0; i < 99; ++i) {
+ char s[41];
+ auto len = length_gen(randu);
+ for (decltype(len) j = 0; j < len; ++j) {
+ s[j] = char_gen(randu);
+ }
+ std::string_view name(s, len);
+ auto spot = ihm.find(name);
+ if (spot != ihm.end() && name != spot->_payload) {
+ miss_p = true;
+ }
+ }
+ REQUIRE(miss_p == false);
+};
+
+TEST_CASE("IntrusiveHashMap Utilities", "[IntrusiveHashMap]") {}
diff --git a/lib/swoc/unit_tests/test_Lexicon.cc b/lib/swoc/unit_tests/test_Lexicon.cc
new file mode 100644
index 0000000000..a9538e0bc0
--- /dev/null
+++ b/lib/swoc/unit_tests/test_Lexicon.cc
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Verizon Media 2020
+/** @file
+
+ Lexicon unit tests.
+*/
+
+#include <iostream>
+
+#include "swoc/Lexicon.h"
+#include "catch.hpp"
+
+// Example code for documentatoin
+// ---
+
+enum class Example { INVALID, Value_0, Value_1, Value_2, Value_3 };
+
+using ExampleNames = swoc::Lexicon<Example>;
+
+namespace {
+
+// C++20: This compiles because one of the lists has more than 2 elements.
+// I think it's a g++ bug - it compiles in clang. The @c TextView constructor being used
+// is marked @c explicit and so it should be discarded. If the constructor is used explicitly
+// then it doesn't compile as intended. Therefore g++ is accepting a constructor that doesn't work.
+// doc.cpp.20.bravo.start
+[[maybe_unused]] ExampleNames Static_Names_Basic{
+ {{Example::Value_0, {"zero", "0", "none"}},
+ {Example::Value_1, {"one", "1"}},
+ {Example::Value_2, {"two", "2"}},
+ {Example::Value_3, {"three", "3"}},
+ {Example::INVALID, {"INVALID"}}}
+};
+// doc.cpp.20.bravo.end
+
+#if 0
+// Verify not using @c with_multi isn't ambiguous.
+// doc.cpp.20.alpha.start
+[[maybe_unused]] ExampleNames Static_Names_Multi{
+ {{Example::Value_0, {"zero", "0"}},
+ {Example::Value_1, {"one", "1"}},
+ {Example::Value_2, {"two", "2"}},
+ {Example::Value_3, {"three", "3"}},
+ {Example::INVALID, {"INVALID"}}}
+};
+// doc.cpp.20.alpha.end
+#endif
+
+// If the type isn't easily accessible.
+[[maybe_unused]] ExampleNames Static_Names_Decl{
+ decltype(Static_Names_Decl)::with_multi{{Example::Value_0, {"zero", "0"}},
+ {Example::Value_1, {"one", "1"}},
+ {Example::Value_2, {"two", "2"}},
+ {Example::Value_3, {"three", "3"}},
+ {Example::INVALID, {"INVALID"}}}
+};
+} // namespace
+
+TEST_CASE("Lexicon", "[libts][Lexicon]") {
+ ExampleNames exnames{
+ ExampleNames::with_multi{{Example::Value_0, {"zero", "0"}},
+ {Example::Value_1, {"one", "1"}},
+ {Example::Value_2, {"two", "2"}},
+ {Example::Value_3, {"three", "3"}}},
+ Example::INVALID, "INVALID"
+ };
+
+ ExampleNames exnames2{
+ {{Example::Value_0, {"zero", "nil"}},
+ {Example::Value_1, {"one", "single", "mono"}},
+ {Example::Value_2, {"two", "double"}},
+ {Example::Value_3, {"three", "triple", "3-tuple"}}},
+ Example::INVALID,
+ "INVALID"
+ };
+
+ // Check constructing with just defaults.
+ ExampleNames def_names_1{Example::INVALID};
+ ExampleNames def_names_2{"INVALID"};
+ ExampleNames def_names_3{Example::INVALID, "INVALID"};
+
+ exnames.set_default(Example::INVALID).set_default("INVALID");
+
+ REQUIRE(exnames[Example::INVALID] == "INVALID");
+ REQUIRE(exnames[Example::Value_0] == "zero");
+ REQUIRE(exnames["zero"] == Example::Value_0);
+ REQUIRE(exnames["Zero"] == Example::Value_0);
+ REQUIRE(exnames["ZERO"] == Example::Value_0);
+ REQUIRE(exnames["one"] == Example::Value_1);
+ REQUIRE(exnames["1"] == Example::Value_1);
+ REQUIRE(exnames["Evil Dave"] == Example::INVALID);
+ REQUIRE(exnames[static_cast<Example>(0xBADD00D)] == "INVALID");
+ REQUIRE(exnames[exnames[static_cast<Example>(0xBADD00D)]] == Example::INVALID);
+
+ REQUIRE(def_names_1["zero"] == Example::INVALID);
+ REQUIRE(def_names_2[Example::Value_0] == "INVALID");
+ REQUIRE(def_names_3["zero"] == Example::INVALID);
+ REQUIRE(def_names_3[Example::Value_0] == "INVALID");
+
+ enum class Radio { INVALID, ALPHA, BRAVO, CHARLIE, DELTA };
+ using Lex = swoc::Lexicon<Radio>;
+ Lex lex(Lex::with_multi{
+ {Radio::INVALID, {"Invalid"} },
+ {Radio::ALPHA, {"Alpha"} },
+ {Radio::BRAVO, {"Bravo", "Beta"}},
+ {Radio::CHARLIE, {"Charlie"} },
+ {Radio::DELTA, {"Delta"} }
+ });
+
+ // test structured binding for iteration.
+ for ([[maybe_unused]] auto const &[key, name] : lex) {
+ }
+};
+
+// ---
+// End example code.
+
+enum Values { NoValue, LowValue, HighValue, Priceless };
+enum Hex { A, B, C, D, E, F, INVALID };
+
+using ValueLexicon = swoc::Lexicon<Values>;
+using HexLexicon = swoc::Lexicon<Hex>;
+
+TEST_CASE("Lexicon Constructor", "[libts][Lexicon]") {
+ // Construct with a secondary name for NoValue
+ ValueLexicon vl{
+ ValueLexicon::with_multi{{NoValue, {"NoValue", "garbage"}}, {LowValue, {"LowValue"}}}
+ };
+
+ REQUIRE("LowValue" == vl[LowValue]); // Primary name
+ REQUIRE(NoValue == vl["NoValue"]); // Primary name
+ REQUIRE(NoValue == vl["garbage"]); // Secondary name
+ REQUIRE_THROWS_AS(vl["monkeys"], std::domain_error); // No default, so throw.
+ vl.set_default(NoValue); // Put in a default.
+ REQUIRE(NoValue == vl["monkeys"]); // Returns default instead of throw
+ REQUIRE(LowValue == vl["lowVALUE"]); // Check case insensitivity.
+
+ REQUIRE(NoValue == vl["HighValue"]); // Not defined yet.
+ vl.define(HighValue, {"HighValue", "High_Value"}); // Add it.
+ REQUIRE(HighValue == vl["HighValue"]); // Verify it's there and is case insensitive.
+ REQUIRE(HighValue == vl["highVALUE"]);
+ REQUIRE(HighValue == vl["HIGH_VALUE"]);
+ REQUIRE("HighValue" == vl[HighValue]); // Verify value -> primary name.
+
+ // A few more checks on primary/secondary.
+ REQUIRE(NoValue == vl["Priceless"]);
+ REQUIRE(NoValue == vl["unique"]);
+ vl.define(Priceless, "Priceless", "Unique");
+ REQUIRE("Priceless" == vl[Priceless]);
+ REQUIRE(Priceless == vl["unique"]);
+
+ // Check default handlers.
+ using LL = swoc::Lexicon<Hex>;
+ bool bad_value_p = false;
+ LL ll_1({
+ {A, "A"},
+ {B, "B"},
+ {C, "C"},
+ {E, "E"}
+ });
+ ll_1.set_default([&bad_value_p](std::string_view name) mutable -> Hex {
+ bad_value_p = true;
+ return INVALID;
+ });
+ ll_1.set_default([&bad_value_p](Hex value) mutable -> std::string_view {
+ bad_value_p = true;
+ return "INVALID";
+ });
+ REQUIRE(bad_value_p == false);
+ REQUIRE(INVALID == ll_1["F"]);
+ REQUIRE(bad_value_p == true);
+ bad_value_p = false;
+ REQUIRE("INVALID" == ll_1[F]);
+ REQUIRE(bad_value_p == true);
+ bad_value_p = false;
+ // Verify that INVALID / "INVALID" are equal because of the default handlers.
+ REQUIRE("INVALID" == ll_1[INVALID]);
+ REQUIRE(INVALID == ll_1["INVALID"]);
+ REQUIRE(bad_value_p == true);
+ // Define the value/name, verify the handlers are *not* invoked.
+ ll_1.define(INVALID, "INVALID");
+ bad_value_p = false;
+ REQUIRE("INVALID" == ll_1[INVALID]);
+ REQUIRE(INVALID == ll_1["INVALID"]);
+ REQUIRE(bad_value_p == false);
+
+ ll_1.define({D, "D"}); // Pair style
+ ll_1.define(LL::Definition{
+ F,
+ {"F", "0xf"}
+ }); // Definition style
+ REQUIRE(ll_1[D] == "D");
+ REQUIRE(ll_1["0XF"] == F);
+
+ // iteration
+ std::bitset<INVALID + 1> mark;
+ for (auto [value, name] : ll_1) {
+ if (mark[value]) {
+ std::cerr << "Lexicon: " << name << ':' << value << " double iterated" << std::endl;
+ mark.reset();
+ break;
+ }
+ mark[value] = true;
+ }
+ REQUIRE(mark.all());
+
+ ValueLexicon v2(std::move(vl));
+ REQUIRE(vl.count() == 0);
+
+ REQUIRE("LowValue" == v2[LowValue]); // Primary name
+ REQUIRE(NoValue == v2["NoValue"]); // Primary name
+ REQUIRE(NoValue == v2["garbage"]); // Secondary name
+
+ REQUIRE(HighValue == v2["highVALUE"]);
+ REQUIRE(HighValue == v2["HIGH_VALUE"]);
+ REQUIRE("HighValue" == v2[HighValue]); // Verify value -> primary name.
+
+ // A few more checks on primary/secondary.
+ REQUIRE("Priceless" == v2[Priceless]);
+ REQUIRE(Priceless == v2["unique"]);
+};
+
+TEST_CASE("Lexicon Constructor 2", "[libts][Lexicon]") {
+ // Check the various construction cases
+ // No defaults, value default, name default, both, both the other way.
+ const HexLexicon v1(HexLexicon::with_multi{
+ {A, {"A", "ten"} },
+ {B, {"B", "eleven"}}
+ });
+
+ const HexLexicon v2(
+ HexLexicon::with_multi{
+ {A, {"A", "ten"} },
+ {B, {"B", "eleven"}}
+ },
+ INVALID);
+
+ const HexLexicon v3(
+ HexLexicon::with_multi{
+ {A, {"A", "ten"} },
+ {B, {"B", "eleven"}}
+ },
+ "Invalid");
+
+ const HexLexicon v4(
+ HexLexicon::with_multi{
+ {A, {"A", "ten"} },
+ {B, {"B", "eleven"}}
+ },
+ "Invalid", INVALID);
+
+ const HexLexicon v5{
+ HexLexicon::with_multi{{A, {"A", "ten"}}, {B, {"B", "eleven"}}},
+ INVALID, "Invalid"
+ };
+
+ const HexLexicon v6(
+ HexLexicon::with_multi{
+ {A, {"A", "ten"} },
+ {B, {"B", "eleven"}}
+ },
+ {INVALID});
+
+ REQUIRE(v1["a"] == A);
+ REQUIRE(v2["q"] == INVALID);
+ REQUIRE(v3[C] == "Invalid");
+ REQUIRE(v4["q"] == INVALID);
+ REQUIRE(v4[C] == "Invalid");
+ REQUIRE(v5["q"] == INVALID);
+ REQUIRE(v5[C] == "Invalid");
+
+ // Y! usages.
+ static constexpr unsigned INVALID_LOCATION = std::numeric_limits<unsigned>::max();
+ [[maybe_unused]] swoc::Lexicon<unsigned> _locations1{INVALID_LOCATION};
+ [[maybe_unused]] swoc::Lexicon<unsigned> _locations2{{INVALID_LOCATION}};
+}
diff --git a/lib/swoc/unit_tests/test_MemArena.cc b/lib/swoc/unit_tests/test_MemArena.cc
new file mode 100644
index 0000000000..e98a982cc3
--- /dev/null
+++ b/lib/swoc/unit_tests/test_MemArena.cc
@@ -0,0 +1,663 @@
+/** @file
+
+ MemArena unit tests.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <array>
+#include <string_view>
+#include <string>
+#include <map>
+#include <set>
+#include <random>
+
+#include "swoc/MemArena.h"
+#include "swoc/TextView.h"
+#include "catch.hpp"
+
+using swoc::MemSpan;
+using swoc::MemArena;
+using swoc::FixedArena;
+using std::string_view;
+using swoc::TextView;
+using namespace std::literals;
+
+static constexpr std::string_view CHARS{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/."};
+std::uniform_int_distribution<short> char_gen{0, short(CHARS.size() - 1)};
+std::minstd_rand randu;
+
+namespace {
+TextView
+localize(MemArena &arena, TextView const &view) {
+ auto span = arena.alloc(view.size()).rebind<char>();
+ memcpy(span, view);
+ return span;
+}
+} // namespace
+
+TEST_CASE("MemArena generic", "[libswoc][MemArena]") {
+ swoc::MemArena arena{64};
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.reserved_size() == 0);
+ arena.alloc(0);
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.reserved_size() >= 64);
+ REQUIRE(arena.remaining() >= 64);
+
+ swoc::MemSpan span1 = arena.alloc(32);
+ REQUIRE(span1.size() == 32);
+ REQUIRE(arena.remaining() >= 32);
+
+ swoc::MemSpan span2 = arena.alloc(32);
+ REQUIRE(span2.size() == 32);
+
+ REQUIRE(span1.data() != span2.data());
+ REQUIRE(arena.size() == 64);
+
+ auto extent{arena.reserved_size()};
+ span1 = arena.alloc(128);
+ REQUIRE(extent < arena.reserved_size());
+
+ arena.clear();
+ arena.alloc(17);
+ span1 = arena.alloc(16, 8);
+ REQUIRE((uintptr_t(span1.data()) & 0x7) == 0);
+ REQUIRE(span1.size() == 16);
+ span2 = arena.alloc(16, 16);
+ REQUIRE((uintptr_t(span2.data()) & 0xF) == 0);
+ REQUIRE(span2.size() == 16);
+ REQUIRE(span2.data() >= span1.data_end());
+}
+
+TEST_CASE("MemArena freeze and thaw", "[libswoc][MemArena]") {
+ MemArena arena;
+ MemSpan span1{arena.alloc(1024)};
+ REQUIRE(span1.size() == 1024);
+ REQUIRE(arena.size() == 1024);
+ REQUIRE(arena.reserved_size() >= 1024);
+
+ arena.freeze();
+
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.allocated_size() == 1024);
+ REQUIRE(arena.reserved_size() >= 1024);
+
+ arena.thaw();
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.allocated_size() == 0);
+ REQUIRE(arena.reserved_size() == 0);
+
+ span1 = arena.alloc(1024);
+ arena.freeze();
+ auto extent{arena.reserved_size()};
+ arena.alloc(512);
+ REQUIRE(arena.reserved_size() > extent); // new extent should be bigger.
+ arena.thaw();
+ REQUIRE(arena.size() == 512);
+ REQUIRE(arena.reserved_size() >= 1024);
+
+ arena.clear();
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.reserved_size() == 0);
+
+ span1 = arena.alloc(262144);
+ arena.freeze();
+ extent = arena.reserved_size();
+ arena.alloc(512);
+ REQUIRE(arena.reserved_size() > extent); // new extent should be bigger.
+ arena.thaw();
+ REQUIRE(arena.size() == 512);
+ REQUIRE(arena.reserved_size() >= 262144);
+
+ arena.clear();
+
+ span1 = arena.alloc(262144);
+ extent = arena.reserved_size();
+ arena.freeze();
+ for (int i = 0; i < 262144 / 512; ++i)
+ arena.alloc(512);
+ REQUIRE(arena.reserved_size() > extent); // Bigger while frozen memory is still around.
+ arena.thaw();
+ REQUIRE(arena.size() == 262144);
+ REQUIRE(arena.reserved_size() == extent); // should be identical to before freeze.
+
+ arena.alloc(512);
+ arena.alloc(768);
+ arena.freeze(32000);
+ arena.thaw();
+ arena.alloc(0);
+ REQUIRE(arena.reserved_size() >= 32000);
+ REQUIRE(arena.reserved_size() < 2 * 32000);
+}
+
+TEST_CASE("MemArena helper", "[libswoc][MemArena]") {
+ struct Thing {
+ int ten{10};
+ std::string name{"name"};
+
+ Thing() {}
+ Thing(int x) : ten(x) {}
+ Thing(std::string const &s) : name(s) {}
+ Thing(int x, std::string_view s) : ten(x), name(s) {}
+ Thing(std::string const &s, int x) : ten(x), name(s) {}
+ };
+
+ swoc::MemArena arena{256};
+ REQUIRE(arena.size() == 0);
+ swoc::MemSpan s = arena.alloc(56).rebind<char>();
+ REQUIRE(arena.size() == 56);
+ REQUIRE(arena.remaining() >= 200);
+ void *ptr = s.begin();
+
+ REQUIRE(arena.contains((char *)ptr));
+ REQUIRE(arena.contains((char *)ptr + 100)); // even though span isn't this large, this pointer should still be in arena
+ REQUIRE(!arena.contains((char *)ptr + 300));
+ REQUIRE(!arena.contains((char *)ptr - 1));
+
+ arena.freeze(128);
+ REQUIRE(arena.contains((char *)ptr));
+ REQUIRE(arena.contains((char *)ptr + 100));
+ swoc::MemSpan s2 = arena.alloc(10).rebind<char>();
+ void *ptr2 = s2.begin();
+ REQUIRE(arena.contains((char *)ptr));
+ REQUIRE(arena.contains((char *)ptr2));
+ REQUIRE(arena.allocated_size() == 56 + 10);
+
+ arena.thaw();
+ REQUIRE(!arena.contains((char *)ptr));
+ REQUIRE(arena.contains((char *)ptr2));
+
+ Thing *thing_one{arena.make<Thing>()};
+
+ REQUIRE(thing_one->ten == 10);
+ REQUIRE(thing_one->name == "name");
+
+ thing_one = arena.make<Thing>(17, "bob"sv);
+
+ REQUIRE(thing_one->name == "bob");
+ REQUIRE(thing_one->ten == 17);
+
+ thing_one = arena.make<Thing>("Dave", 137);
+
+ REQUIRE(thing_one->name == "Dave");
+ REQUIRE(thing_one->ten == 137);
+
+ thing_one = arena.make<Thing>(9999);
+
+ REQUIRE(thing_one->ten == 9999);
+ REQUIRE(thing_one->name == "name");
+
+ thing_one = arena.make<Thing>("Persia");
+
+ REQUIRE(thing_one->ten == 10);
+ REQUIRE(thing_one->name == "Persia");
+}
+
+TEST_CASE("MemArena large alloc", "[libswoc][MemArena]") {
+ swoc::MemArena arena;
+ auto s = arena.alloc(4000);
+ REQUIRE(s.size() == 4000);
+
+ decltype(s) s_a[10];
+ s_a[0] = arena.alloc(100);
+ s_a[1] = arena.alloc(200);
+ s_a[2] = arena.alloc(300);
+ s_a[3] = arena.alloc(400);
+ s_a[4] = arena.alloc(500);
+ s_a[5] = arena.alloc(600);
+ s_a[6] = arena.alloc(700);
+ s_a[7] = arena.alloc(800);
+ s_a[8] = arena.alloc(900);
+ s_a[9] = arena.alloc(1000);
+
+ // ensure none of the spans have any overlap in memory.
+ for (int i = 0; i < 10; ++i) {
+ s = s_a[i];
+ for (int j = i + 1; j < 10; ++j) {
+ REQUIRE(s_a[i].data() != s_a[j].data());
+ }
+ }
+}
+
+TEST_CASE("MemArena block allocation", "[libswoc][MemArena]") {
+ swoc::MemArena arena{64};
+ swoc::MemSpan s = arena.alloc(32).rebind<char>();
+ swoc::MemSpan s2 = arena.alloc(16).rebind<char>();
+ swoc::MemSpan s3 = arena.alloc(16).rebind<char>();
+
+ REQUIRE(s.size() == 32);
+ REQUIRE(arena.allocated_size() == 64);
+
+ REQUIRE(arena.contains((char *)s.begin()));
+ REQUIRE(arena.contains((char *)s2.begin()));
+ REQUIRE(arena.contains((char *)s3.begin()));
+
+ REQUIRE((char *)s.begin() + 32 == (char *)s2.begin());
+ REQUIRE((char *)s.begin() + 48 == (char *)s3.begin());
+ REQUIRE((char *)s2.begin() + 16 == (char *)s3.begin());
+
+ REQUIRE(s.end() == s2.begin());
+ REQUIRE(s2.end() == s3.begin());
+ REQUIRE((char *)s.begin() + 64 == s3.end());
+}
+
+TEST_CASE("MemArena full blocks", "[libswoc][MemArena]") {
+ // couple of large allocations - should be exactly sized in the generation.
+ size_t init_size = 32000;
+ swoc::MemArena arena(init_size);
+
+ MemSpan m1{arena.alloc(init_size - 64).rebind<uint8_t>()};
+ MemSpan m2{arena.alloc(32000).rebind<unsigned char>()};
+ MemSpan m3{arena.alloc(64000).rebind<char>()};
+
+ REQUIRE(arena.remaining() >= 64);
+ REQUIRE(arena.reserved_size() > 32000 + 64000 + init_size);
+ REQUIRE(arena.reserved_size() < 2 * (32000 + 64000 + init_size));
+
+ // Let's see if that memory is really there.
+ memset(m1, 0xa5);
+ memset(m2, 0xc2);
+ memset(m3, 0x56);
+
+ REQUIRE(std::all_of(m1.begin(), m1.end(), [](uint8_t c) { return 0xa5 == c; }));
+ REQUIRE(std::all_of(m2.begin(), m2.end(), [](unsigned char c) { return 0xc2 == c; }));
+ REQUIRE(std::all_of(m3.begin(), m3.end(), [](char c) { return 0x56 == c; }));
+}
+
+TEST_CASE("MemArena esoterica", "[libswoc][MemArena]") {
+ MemArena a1;
+ MemSpan<char> span;
+
+ {
+ MemArena alpha{1020};
+ alpha.alloc(1);
+ REQUIRE(alpha.remaining() >= 1019);
+ }
+
+ {
+ MemArena alpha{4092};
+ alpha.alloc(1);
+ REQUIRE(alpha.remaining() >= 4091);
+ }
+
+ {
+ MemArena alpha{4096};
+ alpha.alloc(1);
+ REQUIRE(alpha.remaining() >= 4095);
+ }
+
+ {
+ MemArena a2{512};
+ span = a2.alloc(128).rebind<char>();
+ REQUIRE(a2.contains(span.data()));
+ a1 = std::move(a2);
+ }
+ REQUIRE(a1.contains(span.data()));
+ REQUIRE(a1.remaining() >= 384);
+
+ {
+ MemArena *arena = MemArena::construct_self_contained();
+ arena->~MemArena();
+ }
+
+ {
+ MemArena *arena = MemArena::construct_self_contained();
+ MemArena::destroyer(arena);
+ }
+
+ {
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena(MemArena::construct_self_contained(),
+ [](MemArena *arena) -> void { arena->~MemArena(); });
+ static constexpr unsigned MAX = 512;
+ std::uniform_int_distribution<unsigned> length_gen{6, MAX};
+ char buffer[MAX];
+ for (unsigned i = 0; i < 50; ++i) {
+ auto n = length_gen(randu);
+ for (unsigned k = 0; k < n; ++k) {
+ buffer[k] = CHARS[char_gen(randu)];
+ }
+ localize(*arena, {buffer, n});
+ }
+ // Really, at this point just make sure there's no memory corruption on destruction.
+ }
+
+ { // as previous but delay construction. Use internal functor instead of a lambda.
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena(nullptr, MemArena::destroyer);
+ arena.reset(MemArena::construct_self_contained());
+ static constexpr unsigned MAX = 512;
+ std::uniform_int_distribution<unsigned> length_gen{6, MAX};
+ char buffer[MAX];
+ for (unsigned i = 0; i < 50; ++i) {
+ auto n = length_gen(randu);
+ for (unsigned k = 0; k < n; ++k) {
+ buffer[k] = CHARS[char_gen(randu)];
+ }
+ localize(*arena, {buffer, n});
+ }
+ // Really, at this point just make sure there's no memory corruption on destruction.
+ }
+
+ { // Construct immediately in the unique pointer.
+ MemArena::unique_ptr arena(MemArena::construct_self_contained(), MemArena::destroyer);
+ static constexpr unsigned MAX = 512;
+ std::uniform_int_distribution<unsigned> length_gen{6, MAX};
+ char buffer[MAX];
+ for (unsigned i = 0; i < 50; ++i) {
+ auto n = length_gen(randu);
+ for (unsigned k = 0; k < n; ++k) {
+ buffer[k] = CHARS[char_gen(randu)];
+ }
+ localize(*arena, {buffer, n});
+ }
+ // Really, at this point just make sure there's no memory corruption on destruction.
+ }
+
+ { // as previous but delay construction. Use destroy_at instead of a lambda.
+ MemArena::unique_ptr arena(nullptr, MemArena::destroyer);
+ arena.reset(MemArena::construct_self_contained());
+ }
+
+ { // And what if the arena is never constructed?
+ struct Thing {
+ int x;
+ std::unique_ptr<MemArena, void (*)(MemArena *)> arena{nullptr, std::destroy_at<MemArena>};
+ } thing;
+ thing.x = 56; // force access to instance.
+ }
+}
+
+// --- temporary allocation
+TEST_CASE("MemArena temporary", "[libswoc][MemArena][tmp]") {
+ MemArena arena;
+ std::vector<std::string_view> strings;
+
+ static constexpr short MAX{8000};
+ static constexpr int N{100};
+
+ std::uniform_int_distribution<unsigned> length_gen{100, MAX};
+ std::array<char, MAX> url;
+
+ REQUIRE(arena.remaining() == 0);
+ int i;
+ unsigned max{0};
+ for (i = 0; i < N; ++i) {
+ auto n = length_gen(randu);
+ max = std::max(max, n);
+ arena.require(n);
+ auto span = arena.remnant().rebind<char>();
+ if (span.size() < n)
+ break;
+ for (auto j = n; j > 0; --j) {
+ span[j - 1] = url[j - 1] = CHARS[char_gen(randu)];
+ }
+ if (string_view{span.data(), n} != string_view{url.data(), n})
+ break;
+ }
+ REQUIRE(i == N); // did all the loops.
+ REQUIRE(arena.size() == 0); // nothing actually allocated.
+ // Hard to get a good value, but shouldn't be more than twice.
+ REQUIRE(arena.reserved_size() < 2 * MAX);
+ // Should be able to allocate at least the longest string without increasing the reserve size.
+ unsigned rsize = arena.reserved_size();
+ auto count = max;
+ std::uniform_int_distribution<unsigned> alloc_size{32, 128};
+ while (count >= 128) { // at least the max distribution value
+ auto k = alloc_size(randu);
+ arena.alloc(k);
+ count -= k;
+ }
+ REQUIRE(arena.reserved_size() == rsize);
+
+ // Check for switching full blocks - calculate something like the total free space
+ // and then try to allocate most of it without increasing the reserved size.
+ count = rsize - (max - count);
+ while (count >= 128) {
+ auto k = alloc_size(randu);
+ arena.alloc(k);
+ count -= k;
+ }
+ REQUIRE(arena.reserved_size() == rsize);
+}
+
+TEST_CASE("FixedArena", "[libswoc][FixedArena]") {
+ struct Thing {
+ int x = 0;
+ std::string name;
+ };
+ MemArena arena;
+ FixedArena<Thing> fa{arena};
+
+ [[maybe_unused]] Thing *one = fa.make();
+ Thing *two = fa.make();
+ two->x = 17;
+ two->name = "Bob";
+ fa.destroy(two);
+ Thing *three = fa.make();
+ REQUIRE(three == two); // reused instance.
+ REQUIRE(three->x == 0); // but reconstructed.
+ REQUIRE(three->name.empty() == true);
+ fa.destroy(three);
+ std::array<Thing *, 17> things;
+ for (auto &ptr : things) {
+ ptr = fa.make();
+ }
+ two = things[things.size() - 1];
+ for (auto &ptr : things) {
+ fa.destroy(ptr);
+ }
+ three = fa.make();
+ REQUIRE(two == three);
+};
+
+TEST_CASE("MemArena disard", "[libswoc][MemArena][discard]") {
+ MemArena a{512};
+ a.require(0); // force allocation.
+ auto x = a.remaining();
+ CHECK(x >= 512);
+ auto span_1 = a.alloc(256);
+ REQUIRE(a.remaining() == (x-256));
+ a.discard(span_1);
+ CHECK(a.remaining() == x);
+ span_1 = a.alloc(100);
+ auto span_2 = a.alloc(50);
+ auto span_3 = a.alloc(50);
+ CHECK(a.remaining() == x - 200);
+ a.discard(span_3);
+ CHECK(a.remaining() == x - 150);
+ a.discard(span_1); // expected to fail.
+ CHECK(a.remaining() == x - 150);
+ a.discard(span_2);
+ CHECK(a.remaining() == x - 100);
+
+ a.discard(512);
+ CHECK(a.remaining() == x);
+
+ auto b1 = a.alloc(400);
+ span_1 = a.alloc(x - 400);
+ CHECK(a.remaining() == 0);
+ CHECK(a.allocated_size() == x);
+
+ span_2 = a.alloc(50);
+ auto b2n = a.remaining();
+ REQUIRE(b2n > 50);
+ a.discard(span_2);
+ REQUIRE(a.remaining() == b2n + span_2.size());
+ REQUIRE(a.allocated_size() == span_1.size() + b1.size());
+ a.discard(b1); // expected to fail.
+ REQUIRE(a.remaining() == b2n + span_2.size());
+ REQUIRE(a.allocated_size() == span_1.size() + b1.size());
+ a.discard(span_1);
+ REQUIRE(a.allocated_size() == b1.size());
+
+ // Try to exercise "last full block" logic.
+ a.clear(512);
+ span_1 = a.alloc(a.remaining()); // fill first block.
+ a.require(1);
+ span_2 = a.alloc(a.remaining()); // fill another block.
+ span_3 = a.alloc(100); // force another block.
+ [[maybe_unused]] auto span_4 = a.alloc(a.remaining() - 100); // use most of it.
+ auto span_5 = a.alloc(100); // fill it.
+ REQUIRE(a.remaining() == 0);
+ auto span_6 = a.alloc(100); // force 4th block.
+ REQUIRE(a.remaining() > 0);
+ a.discard(span_6); // make 4th block empty.
+ REQUIRE(a.remaining() != 100);
+ a.discard(span_5);
+ REQUIRE(a.remaining() == 100); // 3rd block pull to front because it's no longer empty.
+}
+
+// RHEL 7 compatibility - std::pmr::string isn't available even though the header exists unless
+// _GLIBCXX_USE_CXX11_ABI is defined and non-zero. It appears to always be defined for the RHEL
+// toolsets, so if undefined that's OK.
+#if __has_include(<memory_resource>) && ( !defined(_GLIBCXX_USE_CXX11_ABI) || _GLIBCXX_USE_CXX11_ABI)
+struct PMR {
+ bool *_flag;
+ PMR(bool &flag) : _flag(&flag) {}
+ PMR(PMR &&that) : _flag(that._flag) { that._flag = nullptr; }
+ ~PMR() {
+ if (_flag)
+ *_flag = true;
+ }
+};
+
+// External container using a MemArena.
+TEST_CASE("PMR 1", "[libswoc][arena][pmr]") {
+ static const std::pmr::string BRAVO{"bravo bravo bravo bravo"}; // avoid small string opt.
+ using C = std::pmr::map<std::pmr::string, PMR>;
+ bool flags[3] = {false, false, false};
+ MemArena arena;
+ {
+ C c{&arena};
+
+ REQUIRE(arena.size() == 0);
+
+ c.insert(C::value_type{"alpha", PMR(flags[0])});
+ c.insert(C::value_type{BRAVO, PMR(flags[1])});
+ c.insert(C::value_type{"charlie", PMR(flags[2])});
+
+ REQUIRE(arena.size() > 0);
+
+ auto spot = c.find(BRAVO);
+ REQUIRE(spot != c.end());
+ REQUIRE(arena.contains(&*spot));
+ REQUIRE(arena.contains(spot->first.data()));
+ }
+ // Check the map was destructed.
+ REQUIRE(flags[0] == true);
+ REQUIRE(flags[1] == true);
+ REQUIRE(flags[2] == true);
+}
+
+// Container inside MemArena, using the MemArena.
+TEST_CASE("PMR 2", "[libswoc][arena][pmr]") {
+ using C = std::pmr::map<std::pmr::string, PMR>;
+ bool flags[3] = {false, false, false};
+ {
+ static const std::pmr::string BRAVO{"bravo bravo bravo bravo"}; // avoid small string opt.
+ MemArena arena;
+ REQUIRE(arena.size() == 0);
+ C *c = arena.make<C>(&arena);
+ auto base = arena.size();
+ REQUIRE(base > 0);
+
+ c->insert(C::value_type{"alpha", PMR(flags[0])});
+ c->insert(C::value_type{BRAVO, PMR(flags[1])});
+ c->insert(C::value_type{"charlie", PMR(flags[2])});
+
+ REQUIRE(arena.size() > base);
+
+ auto spot = c->find(BRAVO);
+ REQUIRE(spot != c->end());
+ REQUIRE(arena.contains(&*spot));
+ REQUIRE(arena.contains(spot->first.data()));
+ }
+ // Check the map was not destructed.
+ REQUIRE(flags[0] == false);
+ REQUIRE(flags[1] == false);
+ REQUIRE(flags[2] == false);
+}
+
+// Container inside MemArena, using the MemArena.
+TEST_CASE("PMR 3", "[libswoc][arena][pmr]") {
+ using C = std::pmr::set<std::pmr::string>;
+ MemArena arena;
+ REQUIRE(arena.size() == 0);
+ C *c = arena.make<C>(&arena);
+ auto base = arena.size();
+ REQUIRE(base > 0);
+
+ c->insert("alpha");
+ c->insert("bravo");
+ c->insert("charlie");
+ c->insert("delta");
+ c->insert("foxtrot");
+ c->insert("golf");
+
+ REQUIRE(arena.size() > base);
+
+ c->erase("charlie");
+ c->erase("delta");
+ c->erase("alpha");
+
+ // This includes all of the strings.
+ auto pre = arena.allocated_size();
+ arena.freeze();
+ // Copy the set into the arena.
+ C *gc = arena.make<C>(&arena);
+ *gc = *c;
+ auto frozen = arena.allocated_size();
+ REQUIRE(frozen > pre);
+ // Sparse set should be in the frozen memory, and discarded.
+ arena.thaw();
+ auto post = arena.allocated_size();
+ REQUIRE(frozen > post);
+ REQUIRE(pre > post);
+}
+
+TEST_CASE("MemArena static", "[libswoc][MemArena][static]") {
+ static constexpr size_t SIZE = 2048;
+ std::byte buffer[SIZE];
+ MemArena arena{
+ {buffer, SIZE}
+ };
+
+ REQUIRE(arena.remaining() > 0);
+ REQUIRE(arena.remaining() < SIZE);
+ REQUIRE(arena.size() == 0);
+
+ // Allocate something and make sure it's in the static area.
+ auto span = arena.alloc(1024);
+ REQUIRE(true == (buffer <= span.data() && span.data() < buffer + SIZE));
+ span = arena.remnant(); // require the remnant to still be in the buffer.
+ REQUIRE(true == (buffer <= span.data() && span.data() < buffer + SIZE));
+
+ // This can't fit, must be somewhere other than the buffer.
+ span = arena.alloc(SIZE);
+ REQUIRE(false == (buffer <= span.data() && span.data() < buffer + SIZE));
+
+ MemArena arena2{std::move(arena)};
+ REQUIRE(arena2.size() > 0);
+
+ arena2.freeze();
+ arena2.thaw();
+
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena2.size() == 0);
+ // Now let @a arena2 destruct.
+}
+
+#endif // has memory_resource header.
diff --git a/lib/swoc/unit_tests/test_MemSpan.cc b/lib/swoc/unit_tests/test_MemSpan.cc
new file mode 100644
index 0000000000..63555304da
--- /dev/null
+++ b/lib/swoc/unit_tests/test_MemSpan.cc
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ MemSpan unit tests.
+
+*/
+
+#include "catch.hpp"
+
+#include <iostream>
+#include <vector>
+
+#include "swoc/MemSpan.h"
+#include "swoc/TextView.h"
+#include "swoc/MemArena.h"
+
+using swoc::MemSpan;
+using swoc::TextView;
+using namespace swoc::literals;
+
+TEST_CASE("MemSpan", "[libswoc][MemSpan]") {
+ int32_t idx[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ char buff[1024];
+
+ MemSpan<char> span(buff, sizeof(buff));
+ MemSpan<char> left = span.prefix(512);
+ memset(span, ' ');
+ REQUIRE(left.size() == 512);
+ REQUIRE(span.size() == 1024);
+ span.remove_prefix(512);
+ REQUIRE(span.size() == 512);
+ REQUIRE(left.end() == span.begin());
+
+ left.assign(buff, sizeof(buff));
+ span = left.suffix(768);
+ REQUIRE(span.size() == 768);
+ left.remove_suffix(768);
+ REQUIRE(left.end() == span.begin());
+ REQUIRE(left.size() + span.size() == 1024);
+
+ MemSpan<int32_t> idx_span(idx);
+ REQUIRE(idx_span.size() == 11);
+ REQUIRE(idx_span.data_size() == sizeof(idx));
+ REQUIRE(idx_span.data() == idx);
+
+ auto sp2 = idx_span.rebind<int16_t>();
+ REQUIRE(sp2.data_size() == idx_span.data_size());
+ REQUIRE(sp2.size() == 2 * idx_span.size());
+ REQUIRE(sp2[0] == 0);
+ REQUIRE(sp2[1] == 0);
+ // exactly one of { le, be } must be true.
+ bool le = sp2[2] == 1 && sp2[3] == 0;
+ bool be = sp2[2] == 0 && sp2[3] == 1;
+ REQUIRE(le != be);
+ auto idx2 = sp2.rebind<int32_t>(); // still the same if converted back to original?
+ REQUIRE(idx_span.is_same(idx2));
+
+ // Verify attempts to rebind on non-integral sized arrays fails.
+ span.assign(buff, 1022);
+ REQUIRE(span.data_size() == 1022);
+ REQUIRE(span.size() == 1022);
+ auto vs = span.rebind<void>();
+ REQUIRE_THROWS_AS(span.rebind<uint32_t>(), std::invalid_argument);
+ REQUIRE_THROWS_AS(vs.rebind<uint32_t>(), std::invalid_argument);
+ vs.rebind<void>(); // check for void -> void rebind compiling.
+ REQUIRE_FALSE(std::is_const_v<decltype(vs)::value_type>);
+
+ // Check for defaulting to a void rebind.
+ auto vsv = span.rebind();
+ REQUIRE(vsv.size() == 1022);
+ // default rebind of non-const type should be non-const (i.e. void).
+ REQUIRE_FALSE(std::is_const_v<decltype(vs)::value_type>);
+ auto vcs = vs.rebind<void const>();
+ REQUIRE(vcs.size() == 1022);
+ REQUIRE(std::is_const_v<decltype(vcs)::value_type>);
+ MemSpan<char const> char_cv{buff, 64};
+ auto void_cv = char_cv.rebind();
+ REQUIRE(std::is_const_v<decltype(void_cv)::value_type>);
+
+ // Check for assignment to void.
+ vs = span;
+ REQUIRE(vs.size() == 1022);
+
+ // Test C array constructors.
+ MemSpan<char> a{buff};
+ REQUIRE(a.size() == sizeof(buff));
+ REQUIRE(a.data() == buff);
+ float floats[] = {1.1, 2.2, 3.3, 4.4, 5.5};
+ MemSpan<float> fspan{floats};
+ REQUIRE(fspan.size() == 5);
+ REQUIRE(fspan[3] == 4.4f);
+ MemSpan<float> f2span{floats, floats + 5};
+ REQUIRE(fspan.data() == f2span.data());
+ REQUIRE(fspan.size() == f2span.size());
+ REQUIRE(fspan.is_same(f2span));
+
+ // Deduction guides for char because of there being so many choices.
+ MemSpan da{buff};
+ REQUIRE(a.size() == sizeof(buff));
+ REQUIRE(a.data() == buff);
+
+ unsigned char ucb[512];
+ MemSpan ucspan{ucb};
+ memset(ucspan, 0);
+ REQUIRE(ucspan[0] == 0);
+ REQUIRE(ucspan[511] == 0);
+ REQUIRE(ucspan[111] == 0);
+ REQUIRE(ucb[0] == 0);
+ REQUIRE(ucb[511] == 0);
+ ucspan.remove_suffix(1);
+ ucspan.remove_prefix(1);
+ memset(ucspan, '@');
+ REQUIRE(ucspan[0] == '@');
+ REQUIRE(ucspan[509] == '@');
+ REQUIRE(ucb[0] == 0);
+ REQUIRE(ucb[511] == 0);
+ REQUIRE(ucb[510] == '@');
+};
+
+TEST_CASE("MemSpan modifiers", "[libswoc][MemSpan]") {
+ std::string text{"Evil Dave Rulz"};
+ char *pre = text.data();
+ char *post = text.data() + text.size();
+
+ SECTION("Typed") {
+ MemSpan span{text};
+
+ REQUIRE(0 == memcmp(span.clip_prefix(5), MemSpan(pre, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan(pre + 5, text.size() - 5)));
+ span.assign(text.data(), text.size());
+ REQUIRE(0 == memcmp(span.clip_suffix(5), MemSpan(post - 5, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan(pre, text.size() - 5)));
+
+ MemSpan s1{"Evil Dave Rulz"};
+ REQUIRE(s1.size() == 14); // terminal nul is not in view.
+ uint8_t bytes[]{5, 4, 3, 2, 1, 0};
+ MemSpan s2{bytes};
+ REQUIRE(s2.size() == sizeof(bytes)); // terminal nul is in view
+ }
+
+ SECTION("void") {
+ MemSpan<void> span{text};
+
+ REQUIRE(0 == memcmp(span.clip_prefix(5), MemSpan<void>(pre, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan<void>(pre + 5, text.size() - 5)));
+ span.assign(text.data(), text.size());
+ REQUIRE(0 == memcmp(span.clip_suffix(5), MemSpan<void>(post - 5, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan<void>(pre, text.size() - 5)));
+
+ // By design, MemSpan<void> won't construct from a literal string because it's const.
+ // MemSpan<void> s1{"Evil Dave Rulz"}; // Should not compile.
+
+ uint8_t bytes[]{5, 4, 3, 2, 1, 0};
+ MemSpan<void> s2{bytes};
+ REQUIRE(s2.size() == sizeof(bytes)); // terminal nul is in view
+ }
+
+ SECTION("void const") {
+ MemSpan<void const> span{text};
+
+ REQUIRE(0 == memcmp(span.clip_prefix(5), MemSpan<void const>(pre, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan<void const>(pre + 5, text.size() - 5)));
+ span.assign(text.data(), text.size());
+ REQUIRE(0 == memcmp(span.clip_suffix(5), MemSpan<void const>(post - 5, 5)));
+ REQUIRE(0 == memcmp(span, MemSpan<void const>(pre, text.size() - 5)));
+
+ MemSpan<void const> s1{"Evil Dave Rulz"};
+ REQUIRE(s1.size() == 14); // terminal nul is not in view.
+ uint8_t bytes[]{5, 4, 3, 2, 1, 0};
+ MemSpan<void const> s2{bytes};
+ REQUIRE(s2.size() == sizeof(bytes)); // terminal nul is in view
+ }
+}
+
+TEST_CASE("MemSpan construct", "[libswoc][MemSpan]") {
+ static unsigned counter = 0;
+ struct Thing {
+ Thing(TextView s) : _s(s) { ++counter; }
+ ~Thing() { --counter; }
+
+ unsigned _n = 56;
+ std::string _s;
+ };
+
+ char buff[sizeof(Thing) * 7];
+ auto span{MemSpan(buff).rebind<Thing>()};
+
+ span.make("default"_tv);
+ REQUIRE(counter == span.length());
+ REQUIRE(span[2]._s == "default");
+ REQUIRE(span[4]._n == 56);
+ span.destroy();
+ REQUIRE(counter == 0);
+}
+
+TEST_CASE("MemSpan<void>", "[libswoc][MemSpan]") {
+ TextView tv = "bike shed";
+ char buff[1024];
+
+ MemSpan<void> span(buff, sizeof(buff));
+ MemSpan<void const> cspan(span);
+ MemSpan<void const> ccspan(tv.data(), tv.size());
+ CHECK_FALSE(cspan.is_same(ccspan));
+ ccspan = span;
+
+ // auto bad_span = ccspan.rebind<uint8_t>(); // should not compile.
+
+ auto left = span.prefix(512);
+ REQUIRE(left.size() == 512);
+ REQUIRE(span.size() == 1024);
+ span.remove_prefix(512);
+ REQUIRE(span.size() == 512);
+ REQUIRE(left.data_end() == span.data());
+
+ left.assign(buff, sizeof(buff));
+ span = left.suffix(700);
+ REQUIRE(span.size() == 700);
+ left.remove_suffix(700);
+ REQUIRE(left.data_end() == span.data());
+ REQUIRE(left.size() + span.size() == 1024);
+
+ MemSpan<void> a(buff, sizeof(buff));
+ MemSpan<void> b;
+ b = a.align<int>();
+ REQUIRE(b.data() == a.data());
+ REQUIRE(b.size() == a.size());
+
+ b = a.suffix(a.size() - 2).align<int>();
+ REQUIRE(b.data() != a.data());
+ REQUIRE(b.size() != a.size());
+ auto i = a.rebind<int>();
+ REQUIRE(b.data() == i.data() + 1);
+
+ b = a.suffix(a.size() - 2).align(alignof(int));
+ REQUIRE(b.data() == i.data() + 1);
+ REQUIRE(b.rebind<int>().size() == i.size() - 1);
+};
+
+TEST_CASE("MemSpan conversions", "[libswoc][MemSpan]") {
+ std::array<int, 10> a1;
+ std::string_view sv{"Evil Dave"};
+ swoc::TextView tv{sv};
+ std::string str{sv};
+ auto const &ra1 = a1;
+ auto ms1 = MemSpan<int>(a1); // construct from array
+ auto ms2 = MemSpan(a1); // construct from array, deduction guide
+ REQUIRE(ms2.size() == a1.size());
+ auto ms3 = MemSpan<int const>(ra1); // construct from const array
+ REQUIRE(ms3.size() == ra1.size());
+ [[maybe_unused]] auto ms4 = MemSpan(ra1); // construct from const array, deduction guided.
+ // Construct a span of constant from a const ref to an array with non-const type.
+ MemSpan<int const> ms5{ra1};
+ REQUIRE(ms5.size() == ra1.size());
+ // Construct a span of constant from a ref to an array with non-const type.
+ MemSpan<int const> ms6{a1};
+
+ MemSpan<void> va1{a1};
+ REQUIRE(va1.size() == a1.size() * sizeof(*(a1.data())));
+ MemSpan<void const> cva1{a1};
+ REQUIRE(cva1.size() == a1.size() * sizeof(*(a1.data())));
+
+ [[maybe_unused]] MemSpan<int const> c1 = ms1; // Conversion from T to T const.
+
+ MemSpan<char const> c2{sv.data(), sv.size()};
+ [[maybe_unused]] MemSpan<void const> vc2{c2};
+ // Generic construction from STL containers.
+ MemSpan<char const> c3{sv};
+ [[maybe_unused]] MemSpan<char> c7{str};
+ [[maybe_unused]] MemSpan<void> c4{str};
+ auto const &cstr{str};
+ MemSpan<char const> c8{cstr};
+ REQUIRE(c8.size() == cstr.size());
+ // [[maybe_unused]] MemSpan<char> c9{cstr}; // should not compile, const container to non-const span.
+
+ [[maybe_unused]] MemSpan<void const> c5{str};
+ [[maybe_unused]] MemSpan<void const> c6{sv};
+
+ [[maybe_unused]] MemSpan c10{sv};
+ [[maybe_unused]] MemSpan c11{tv};
+
+ char const *args[] = {"alpha", "bravo", "charlie", "delta"};
+ MemSpan<char const *> span_args{args};
+ MemSpan<char const *> span2_args{span_args};
+ REQUIRE(span_args.size() == 4);
+ REQUIRE(span2_args.size() == 4);
+
+ auto f = [&]() -> TextView { return sv; };
+ MemSpan fs1{f()};
+ auto fc = [&]() -> TextView const { return sv; };
+ MemSpan fs2{fc()};
+}
+
+TEST_CASE("MemSpan arena", "[libswoc][MemSpan]") {
+ swoc::MemArena a;
+
+ struct Thing {
+ size_t _n = 0;
+ void *_ptr = nullptr;
+ };
+
+ auto span = a.alloc(sizeof(Thing)).rebind<Thing>();
+ MemSpan<void> raw = span;
+ REQUIRE(raw.size() == sizeof(Thing));
+ MemSpan<void const> craw = raw;
+ REQUIRE(raw.size() == craw.size());
+ craw = span;
+ REQUIRE(raw.size() == craw.size());
+
+ REQUIRE(raw.rebind<Thing>().length() == 1);
+}
diff --git a/lib/swoc/unit_tests/test_Scalar.cc b/lib/swoc/unit_tests/test_Scalar.cc
new file mode 100644
index 0000000000..f7f4479e8f
--- /dev/null
+++ b/lib/swoc/unit_tests/test_Scalar.cc
@@ -0,0 +1,257 @@
+/** @file
+
+ Scalar unit testing.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include "swoc/Scalar.h"
+#include "swoc/bwf_base.h"
+#include "catch.hpp"
+
+using Bytes = swoc::Scalar<1, off_t>;
+using Paragraphs = swoc::Scalar<16, off_t>;
+using KB = swoc::Scalar<1024, off_t>;
+using MB = swoc::Scalar<KB::SCALE * 1024, off_t>;
+
+TEST_CASE("Scalar", "[libswoc][Scalar]") {
+ constexpr static int SCALE = 4096;
+ constexpr static int SCALE_1 = 8192;
+ constexpr static int SCALE_2 = 512;
+
+ using PageSize = swoc::Scalar<SCALE>;
+
+ PageSize pg1(1);
+ REQUIRE(pg1.count() == 1);
+ REQUIRE(pg1.value() == SCALE);
+
+ using Size_1 = swoc::Scalar<SCALE_1>;
+ using Size_2 = swoc::Scalar<SCALE_2>;
+
+ Size_2 sz_a(2);
+ Size_2 sz_b(57);
+ Size_2 sz_c(SCALE_1 / SCALE_2);
+ Size_2 sz_d(29 * SCALE_1 / SCALE_2);
+
+ Size_1 sz = swoc::round_up(sz_a);
+ REQUIRE(sz.count() == 1);
+ sz = swoc::round_down(sz_a);
+ REQUIRE(sz.count() == 0);
+
+ sz = swoc::round_up(sz_b);
+ REQUIRE(sz.count() == 4);
+ sz = swoc::round_down(sz_b);
+ REQUIRE(sz.count() == 3);
+
+ sz = swoc::round_up(sz_c);
+ REQUIRE(sz.count() == 1);
+ sz = swoc::round_down(sz_c);
+ REQUIRE(sz.count() == 1);
+
+ sz = swoc::round_up(sz_d);
+ REQUIRE(sz.count() == 29);
+ sz = swoc::round_down(sz_d);
+ REQUIRE(sz.count() == 29);
+
+ sz.assign(119);
+ sz_b = sz; // Should be OK because SCALE_1 is an integer multiple of SCALE_2
+ // sz = sz_b; // Should not compile.
+ REQUIRE(sz_b.count() == 119 * (SCALE_1 / SCALE_2));
+
+ // Test generic rounding.
+ REQUIRE(120 == swoc::round_up<10>(118));
+ REQUIRE(120 == swoc::round_up<10>(120));
+ REQUIRE(130 == swoc::round_up<10>(121));
+
+ REQUIRE(110 == swoc::round_down<10>(118));
+ REQUIRE(120 == swoc::round_down<10>(120));
+ REQUIRE(120 == swoc::round_down<10>(121));
+
+ REQUIRE(200 == swoc::round_up<100>(118));
+ REQUIRE(1200 == swoc::round_up<100>(1118));
+ REQUIRE(1200 == swoc::round_up<100>(1200));
+ REQUIRE(1300 == swoc::round_up<100>(1210));
+
+ REQUIRE(100 == swoc::round_down<100>(118));
+ REQUIRE(1100 == swoc::round_down<100>(1118));
+ REQUIRE(1200 == swoc::round_down<100>(1200));
+ REQUIRE(1200 == swoc::round_down<100>(1210));
+}
+TEST_CASE("Scalar Factors", "[libswoc][Scalar][factors]") {
+ constexpr static int SCALE_1 = 30;
+ constexpr static int SCALE_2 = 20;
+
+ using Size_1 = swoc::Scalar<SCALE_1>;
+ using Size_2 = swoc::Scalar<SCALE_2>;
+
+ Size_2 sz_a(2);
+ Size_2 sz_b(97);
+
+ Size_1 sz = round_up(sz_a);
+ REQUIRE(sz.count() == 2);
+ sz = round_down(sz_a);
+ REQUIRE(sz.count() == 1);
+
+ sz = swoc::round_up(sz_b);
+ REQUIRE(sz.count() == 65);
+ sz = swoc::round_down(sz_b);
+ REQUIRE(sz.count() == 64);
+
+ swoc::Scalar<9> m_9;
+ swoc::Scalar<4> m_4, m_test;
+
+ m_9.assign(95);
+ // m_4 = m_9; // Should fail to compile with static assert.
+ // m_9 = m_4; // Should fail to compile with static assert.
+
+ m_4 = swoc::round_up(m_9);
+ REQUIRE(m_4.count() == 214);
+ m_4 = swoc::round_down(m_9);
+ REQUIRE(m_4.count() == 213);
+
+ m_4.assign(213);
+ m_9 = swoc::round_up(m_4);
+ REQUIRE(m_9.count() == 95);
+ m_9 = swoc::round_down(m_4);
+ REQUIRE(m_9.count() == 94);
+
+ m_test = m_4; // Verify assignment of identical scale values compiles.
+ REQUIRE(m_test.count() == 213);
+}
+
+TEST_CASE("Scalar Arithmetic", "[libswoc][Scalar][arithmetic]") {
+ using KBytes = swoc::Scalar<1024>;
+ using KiBytes = swoc::Scalar<1024, long int>;
+ using Bytes = swoc::Scalar<1, int64_t>;
+ using MBytes = swoc::Scalar<1024 * KBytes::SCALE>;
+
+ Bytes bytes(96);
+ KBytes kbytes(2);
+ MBytes mbytes(5);
+
+ Bytes z1 = swoc::round_up(bytes + 128);
+ REQUIRE(z1.count() == 224);
+ KBytes z2 = kbytes + kbytes(3);
+ REQUIRE(z2.count() == 5);
+ Bytes z3(bytes);
+ z3 += kbytes;
+ REQUIRE(z3.value() == 2048 + 96);
+ MBytes z4 = mbytes;
+ z4.inc(5);
+ z2 += z4;
+ REQUIRE(z2.value() == (10 << 20) + (5 << 10));
+
+ z1.inc(128);
+ REQUIRE(z1.count() == 352);
+
+ z2.assign(2);
+ z1 = 3 * z2;
+ REQUIRE(z1.count() == 6144);
+ z1 *= 5;
+ REQUIRE(z1.count() == 30720);
+ z1 /= 3;
+ REQUIRE(z1.count() == 10240);
+
+ z2.assign(3148);
+ auto x = z2 + MBytes(1);
+ REQUIRE(x.scale() == z2.scale());
+ REQUIRE(x.count() == 4172);
+
+ z2 = swoc::round_down(262150);
+ REQUIRE(z2.count() == 256);
+
+ z2 = swoc::round_up(262150);
+ REQUIRE(z2.count() == 257);
+
+ KBytes q(swoc::round_down(262150));
+ REQUIRE(q.count() == 256);
+
+ z2 += swoc::round_up(97384);
+ REQUIRE(z2.count() == 353);
+
+ decltype(z2) a = swoc::round_down(z2 + 167229);
+ REQUIRE(a.count() == 516);
+
+ KiBytes k(3148);
+ auto kx = k + MBytes(1);
+ REQUIRE(kx.scale() == k.scale());
+ REQUIRE(kx.count() == 4172);
+
+ k = swoc::round_down(262150);
+ REQUIRE(k.count() == 256);
+
+ k = swoc::round_up(262150);
+ REQUIRE(k.count() == 257);
+
+ KBytes kq(swoc::round_down(262150));
+ REQUIRE(kq.count() == 256);
+
+ k += swoc::round_up(97384);
+ REQUIRE(k.count() == 353);
+
+ decltype(k) ka = swoc::round_down(k + 167229);
+ REQUIRE(ka.count() == 516);
+
+ using StoreBlocks = swoc::Scalar<8 * KB::SCALE, off_t>;
+ using SpanBlocks = swoc::Scalar<127 * MB::SCALE, off_t>;
+
+ StoreBlocks store_b(80759700);
+ SpanBlocks span_b(4968);
+ SpanBlocks delta(1);
+
+ REQUIRE(store_b < span_b);
+ REQUIRE(span_b < store_b + delta);
+ store_b += delta;
+ REQUIRE(span_b < store_b);
+
+ static const off_t N = 7 * 1024;
+ Bytes b(N + 384);
+ KB kb(round_down(b));
+
+ REQUIRE(kb == N);
+ REQUIRE(kb < N + 1);
+ REQUIRE(kb > N - 1);
+
+ REQUIRE(kb < b);
+ REQUIRE(kb <= b);
+ REQUIRE(b > kb);
+ REQUIRE(b >= kb);
+
+ ++kb;
+
+ REQUIRE(b < kb);
+ REQUIRE(b <= kb);
+ REQUIRE(kb > b);
+ REQUIRE(kb >= b);
+}
+
+struct KBytes_tag {
+ static constexpr std::string_view label{" bytes"};
+};
+
+TEST_CASE("Scalar Formatting", "[libswoc][Scalar][bwf]") {
+ using KBytes = swoc::Scalar<1024, long int, KBytes_tag>;
+ using KiBytes = swoc::Scalar<1000, int>;
+
+ KBytes x(12);
+ KiBytes y(12);
+ swoc::LocalBufferWriter<128> w;
+
+ w.print("x is {}", x);
+ REQUIRE(w.view() == "x is 12288 bytes");
+ w.clear().print("y is {}", y);
+ REQUIRE(w.view() == "y is 12000");
+}
diff --git a/lib/swoc/unit_tests/test_TextView.cc b/lib/swoc/unit_tests/test_TextView.cc
new file mode 100644
index 0000000000..ebeb6c23a7
--- /dev/null
+++ b/lib/swoc/unit_tests/test_TextView.cc
@@ -0,0 +1,651 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ TextView unit tests.
+*/
+
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <map>
+#include <unordered_map>
+
+#include "swoc/TextView.h"
+#include "catch.hpp"
+
+using swoc::TextView;
+using namespace std::literals;
+using namespace swoc::literals;
+
+TEST_CASE("TextView Constructor", "[libswoc][TextView]") {
+ static const std::string base = "Evil Dave Rulez!";
+ unsigned ux = base.size();
+ TextView tv(base);
+ TextView a{"Evil Dave Rulez"};
+ TextView b{base.data(), base.size()};
+ TextView c{std::string_view(base)};
+ constexpr TextView d{"Grigor!"sv};
+ TextView e{base.data(), 15};
+ TextView f(base.data(), 15);
+ TextView u{base.data(), ux};
+ TextView g{base.data(), base.data() + base.size()}; // begin/end pointers.
+
+ // Check the various forms of char pointers work unambiguously.
+ TextView bob{"Bob"};
+ std::string dave("dave");
+ REQUIRE(bob == "Bob"_tv); // Attempt to verify @a bob isn't pointing at a temporary.
+ char q[12] = "Bob";
+ TextView t_q{q};
+ REQUIRE(t_q.data() == q); // must point at @a q.
+ char *qp = q;
+ TextView t_qp{qp};
+ REQUIRE(t_qp.data() == qp); // verify pointer is not pointing at a temporary.
+ char const *qcp = "Bob";
+ TextView t_qcp{qcp};
+ REQUIRE(t_qcp.data() == qcp);
+
+ tv = "Delain"; // assign literal.
+ REQUIRE(tv.size() == 6);
+ tv = q; // Assign array.
+ REQUIRE(tv.size() == sizeof(q) - 1); // trailing nul char dropped.
+ tv = qp; // Assign pointer.
+ REQUIRE(tv.data() == qp);
+ tv = qcp; // Assign pointer to const.
+ REQUIRE(tv.data() == qcp);
+ tv = std::string_view(base);
+ REQUIRE(tv.size() == base.size());
+
+ qp = nullptr;
+ REQUIRE(TextView(qp).size() == 0);
+ qcp = nullptr;
+ REQUIRE(TextView(qcp).size() == 0);
+};
+
+TEST_CASE("TextView Operations", "[libswoc][TextView]") {
+ TextView tv{"Evil Dave Rulez"};
+ TextView tv_lower{"evil dave rulez"};
+ TextView nothing;
+ size_t off;
+
+ REQUIRE(tv.find('l') == 3);
+ off = tv.find_if([](char c) { return c == 'D'; });
+ REQUIRE(off == tv.find('D'));
+
+ REQUIRE(tv);
+ REQUIRE(!tv == false);
+ if (nothing) {
+ REQUIRE(nullptr == "bad operator bool on TextView");
+ }
+ REQUIRE(!nothing == true);
+ REQUIRE(nothing.empty() == true);
+
+ REQUIRE(memcmp(tv, tv) == 0);
+ REQUIRE(memcmp(tv, tv_lower) != 0);
+ REQUIRE(strcmp(tv, tv) == 0);
+ REQUIRE(strcmp(tv, tv_lower) != 0);
+ REQUIRE(strcasecmp(tv, tv) == 0);
+ REQUIRE(strcasecmp(tv, tv_lower) == 0);
+ REQUIRE(strcasecmp(nothing, tv) != 0);
+
+ // Check generic construction from a "string like" class.
+ struct Stringy {
+ char const *
+ data() const {
+ return _data;
+ }
+ size_t
+ size() const {
+ return _size;
+ }
+
+ char const *_data = nullptr;
+ size_t _size;
+ };
+
+ char const *stringy_text = "Evil Dave Rulez";
+ Stringy stringy{stringy_text, strlen(stringy_text)};
+
+ // Can construct directly.
+ TextView from_stringy{stringy};
+ REQUIRE(0 == strcmp(from_stringy, stringy_text));
+
+ // Can assign directly.
+ TextView assign_stringy;
+ REQUIRE(assign_stringy.empty() == true);
+ assign_stringy.assign(stringy);
+ REQUIRE(0 == strcmp(assign_stringy, stringy_text));
+
+ // Pass as argument to TextView parameter.
+ auto stringy_f = [&](TextView txt) -> bool { return 0 == strcmp(txt, stringy_text); };
+ REQUIRE(true == stringy_f(stringy));
+ REQUIRE(false == stringy_f(tv_lower));
+}
+
+TEST_CASE("TextView Trimming", "[libswoc][TextView]") {
+ TextView tv(" Evil Dave Rulz ...");
+ TextView tv2{"More Text1234567890"};
+ REQUIRE("Evil Dave Rulz ..." == TextView(tv).ltrim_if(&isspace));
+ REQUIRE(tv2 == TextView{tv2}.ltrim_if(&isspace));
+ REQUIRE("More Text" == TextView{tv2}.rtrim_if(&isdigit));
+ REQUIRE(" Evil Dave Rulz " == TextView(tv).rtrim('.'));
+ REQUIRE("Evil Dave Rulz" == TextView(tv).trim(" ."));
+
+ tv.assign("\r\n");
+ tv.rtrim_if([](char c) -> bool { return c == '\r' || c == '\n'; });
+ REQUIRE(tv.size() == 0);
+
+ tv.assign("...");
+ tv.rtrim('.');
+ REQUIRE(tv.size() == 0);
+
+ tv.assign(".,,.;.");
+ tv.rtrim(";,."_tv);
+ REQUIRE(tv.size() == 0);
+}
+
+TEST_CASE("TextView Find", "[libswoc][TextView]") {
+ TextView addr{"172.29.145.87:5050"};
+ REQUIRE(addr.find(':') == 13);
+ REQUIRE(addr.rfind(':') == 13);
+ REQUIRE(addr.find('.') == 3);
+ REQUIRE(addr.rfind('.') == 10);
+}
+
+TEST_CASE("TextView Affixes", "[libswoc][TextView]") {
+ TextView s; // scratch.
+ TextView tv1("0123456789;01234567890");
+ TextView prefix{tv1.prefix(10)};
+
+ REQUIRE("0123456789" == prefix);
+ REQUIRE("90" == tv1.suffix(2));
+ REQUIRE("67890" == tv1.suffix(5));
+ REQUIRE("4567890" == tv1.suffix(7));
+ REQUIRE(tv1 == tv1.prefix(9999));
+ REQUIRE(tv1 == tv1.suffix(9999));
+
+ TextView tv2 = tv1.prefix_at(';');
+ REQUIRE(tv2 == "0123456789");
+ REQUIRE(tv1.prefix_at('z').empty());
+ REQUIRE(tv1.suffix_at('z').empty());
+
+ s = tv1;
+ REQUIRE(s.remove_prefix(10) == ";01234567890");
+ s = tv1;
+ REQUIRE(s.remove_prefix(9999).empty());
+ s = tv1;
+ REQUIRE(s.remove_suffix(11) == "0123456789;");
+ s = tv1;
+ s.remove_suffix(9999);
+ REQUIRE(s.empty());
+ REQUIRE(s.data() == tv1.data());
+
+ TextView right{tv1};
+ TextView left{right.split_prefix_at(';')};
+ REQUIRE(right.size() == 11);
+ REQUIRE(left.size() == 10);
+
+ TextView tv3 = "abcdefg:gfedcba";
+ left = tv3;
+ right = left.split_suffix_at(";:,");
+ TextView pre{tv3}, post{pre.split_suffix(7)};
+ REQUIRE(right.size() == 7);
+ REQUIRE(left.size() == 7);
+ REQUIRE(left == "abcdefg");
+ REQUIRE(right == "gfedcba");
+
+ TextView addr1{"[fe80::fc54:ff:fe60:d886]"};
+ TextView addr2{"[fe80::fc54:ff:fe60:d886]:956"};
+ TextView addr3{"192.168.1.1:5050"};
+ TextView host{"evil.dave.rulz"};
+
+ TextView t = addr1;
+ ++t;
+ REQUIRE("fe80::fc54:ff:fe60:d886]" == t);
+ TextView a = t.take_prefix_at(']');
+ REQUIRE("fe80::fc54:ff:fe60:d886" == a);
+ REQUIRE(t.empty());
+
+ t = addr2;
+ ++t;
+ a = t.take_prefix_at(']');
+ REQUIRE("fe80::fc54:ff:fe60:d886" == a);
+ REQUIRE(':' == *t);
+ ++t;
+ REQUIRE("956" == t);
+
+ t = addr3;
+ TextView sf{t.suffix_at(':')};
+ REQUIRE("5050" == sf);
+ REQUIRE(t == addr3);
+
+ t = addr3;
+ s = t.split_suffix(4);
+ REQUIRE("5050" == s);
+ REQUIRE("192.168.1.1" == t);
+
+ t = addr3;
+ s = t.split_suffix_at(':');
+ REQUIRE("5050" == s);
+ REQUIRE("192.168.1.1" == t);
+
+ t = addr3;
+ s = t.split_suffix_at('Q');
+ REQUIRE(s.empty());
+ REQUIRE(t == addr3);
+
+ t = addr3;
+ s = t.take_suffix_at(':');
+ REQUIRE("5050" == s);
+ REQUIRE("192.168.1.1" == t);
+
+ t = addr3;
+ s = t.take_suffix_at('Q');
+ REQUIRE(s == addr3);
+ REQUIRE(t.empty());
+
+ REQUIRE(host.suffix_at('.') == "rulz");
+ REQUIRE(true == host.suffix_at(':').empty());
+
+ auto is_sep{[](char c) { return isspace(c) || ',' == c || ';' == c; }};
+ TextView token;
+ t = ";; , ;;one;two,th:ree four,, ; ,,f-ive="sv;
+ // Do an unrolled loop.
+ REQUIRE(!t.ltrim_if(is_sep).empty());
+ REQUIRE(t.take_prefix_if(is_sep) == "one");
+ REQUIRE(!t.ltrim_if(is_sep).empty());
+ REQUIRE(t.take_prefix_if(is_sep) == "two");
+ REQUIRE(!t.ltrim_if(is_sep).empty());
+ REQUIRE(t.take_prefix_if(is_sep) == "th:ree");
+ REQUIRE(!t.ltrim_if(is_sep).empty());
+ REQUIRE(t.take_prefix_if(is_sep) == "four");
+ REQUIRE(!t.ltrim_if(is_sep).empty());
+ REQUIRE(t.take_prefix_if(is_sep) == "f-ive=");
+ REQUIRE(t.empty());
+
+ // Simulate pulling off FQDN pieces in reverse order from a string_view.
+ // Simulates operations in HostLookup.cc, where the use of string_view
+ // necessitates this workaround of failures in the string_view API.
+ std::string_view fqdn{"bob.ne1.corp.ngeo.com"};
+ TextView elt{TextView{fqdn}.take_suffix_at('.')};
+ REQUIRE(elt == "com");
+ fqdn.remove_suffix(std::min(fqdn.size(), elt.size() + 1));
+
+ // Unroll loop for testing.
+ elt = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(elt == "ngeo");
+ fqdn.remove_suffix(std::min(fqdn.size(), elt.size() + 1));
+ elt = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(elt == "corp");
+ fqdn.remove_suffix(std::min(fqdn.size(), elt.size() + 1));
+ elt = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(elt == "ne1");
+ fqdn.remove_suffix(std::min(fqdn.size(), elt.size() + 1));
+ elt = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(elt == "bob");
+ fqdn.remove_suffix(std::min(fqdn.size(), elt.size() + 1));
+ elt = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(elt.empty());
+
+ // Do it again, TextView stle.
+ t = "bob.ne1.corp.ngeo.com";
+ REQUIRE(t.rtrim('.').take_suffix_at('.') == "com"_tv);
+ REQUIRE(t.rtrim('.').take_suffix_at('.') == "ngeo"_tv);
+ REQUIRE(t.rtrim('.').take_suffix_at('.') == "corp"_tv);
+ REQUIRE(t.take_suffix_at('.') == "ne1"_tv);
+ REQUIRE(t.take_suffix_at('.') == "bob"_tv);
+ REQUIRE(t.size() == 0);
+
+ t = "bob.ne1.corp.ngeo.com";
+ REQUIRE(t.remove_suffix_at('.') == "bob.ne1.corp.ngeo"_tv);
+ REQUIRE(t.remove_suffix_at('.') == "bob.ne1.corp"_tv);
+ REQUIRE(t.remove_suffix_at('.') == "bob.ne1"_tv);
+ REQUIRE(t.remove_suffix_at('.') == "bob"_tv);
+ REQUIRE(t.remove_suffix_at('.').size() == 0);
+
+ // Check some edge cases.
+ fqdn = "."sv;
+ token = TextView{fqdn}.take_suffix_at('.');
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.empty());
+
+ s = "."sv;
+ REQUIRE(s.size() == 1);
+ REQUIRE(s.rtrim('.').empty());
+ token = s.take_suffix_at('.');
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.empty());
+
+ s = "."sv;
+ REQUIRE(s.size() == 1);
+ REQUIRE(s.ltrim('.').empty());
+ token = s.take_prefix_at('.');
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.empty());
+
+ s = ".."sv;
+ REQUIRE(s.size() == 2);
+ token = s.take_suffix_at('.');
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.empty());
+ REQUIRE(s.size() == 1);
+
+ // Check for subtle differences with trailing separator
+ token = "one.ex";
+ auto name = token.take_prefix_at('.');
+ REQUIRE(name.size() > 0);
+ REQUIRE(token.size() > 0);
+
+ token = "one";
+ name = token.take_prefix_at('.');
+ REQUIRE(name.size() > 0);
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.data() == name.end());
+
+ token = "one.";
+ name = token.take_prefix_at('.');
+ REQUIRE(name.size() > 0);
+ REQUIRE(token.size() == 0);
+ REQUIRE(token.data() == name.end() + 1);
+
+ auto is_not_alnum = [](char c) { return !isalnum(c); };
+
+ s = "file.cc";
+ REQUIRE(s.suffix_at('.') == "cc");
+ REQUIRE(s.suffix_if(is_not_alnum) == "cc");
+ REQUIRE(s.prefix_at('.') == "file");
+ REQUIRE(s.prefix_if(is_not_alnum) == "file");
+ s.remove_suffix_at('.');
+ REQUIRE(s == "file");
+ s = "file.cc.org.123";
+ REQUIRE(s.suffix_at('.') == "123");
+ REQUIRE(s.prefix_at('.') == "file");
+ s.remove_suffix_if(is_not_alnum);
+ REQUIRE(s == "file.cc.org");
+ s.remove_suffix_at('.');
+ REQUIRE(s == "file.cc");
+ s.remove_prefix_at('.');
+ REQUIRE(s == "cc");
+ s = "file.cc.org.123";
+ s.remove_prefix_if(is_not_alnum);
+ REQUIRE(s == "cc.org.123");
+ s.remove_suffix_at('!');
+ REQUIRE(s.empty());
+ s = "file.cc.org";
+ s.remove_prefix_at('!');
+ REQUIRE(s == "file.cc.org");
+
+ static constexpr TextView ctv{"http://delain.nl/albums/Lucidity.html"};
+ static constexpr TextView ctv_scheme{ctv.prefix(4)};
+ static constexpr TextView ctv_stem{ctv.suffix(4)};
+ static constexpr TextView ctv_host{ctv.substr(7, 9)};
+ REQUIRE(ctv.starts_with("http"_tv) == true);
+ REQUIRE(ctv.ends_with(".html") == true);
+ REQUIRE(ctv.starts_with("https"_tv) == false);
+ REQUIRE(ctv.ends_with(".jpg") == false);
+ REQUIRE(ctv.starts_with_nocase("HttP"_tv) == true);
+ REQUIRE(ctv.starts_with_nocase("HttP") == true);
+ REQUIRE(ctv.starts_with("HttP") == false);
+ REQUIRE(ctv.starts_with("http") == true);
+ REQUIRE(ctv.starts_with('h') == true);
+ REQUIRE(ctv.starts_with('H') == false);
+ REQUIRE(ctv.starts_with_nocase('H') == true);
+ REQUIRE(ctv.starts_with('q') == false);
+ REQUIRE(ctv.starts_with_nocase('Q') == false);
+ REQUIRE(ctv.ends_with("htML"_tv) == false);
+ REQUIRE(ctv.ends_with_nocase("htML"_tv) == true);
+ REQUIRE(ctv.ends_with("htML") == false);
+ REQUIRE(ctv.ends_with_nocase("htML") == true);
+
+ REQUIRE(ctv_scheme == "http"_tv);
+ REQUIRE(ctv_stem == "html"_tv);
+ REQUIRE(ctv_host == "delain.nl"_tv);
+
+ // Checking that constexpr works for this constructor as long as npos isn't used.
+ static constexpr TextView ctv2{"http://delain.nl/albums/Interlude.html", 38};
+ TextView ctv4{"http://delain.nl/albums/Interlude.html", 38};
+ // This doesn't compile because it causes strlen to be called which isn't constexpr compatible.
+ // static constexpr TextView ctv3 {"http://delain.nl/albums/Interlude.html", TextView::npos};
+ // This works because it's not constexpr.
+ TextView ctv3{"http://delain.nl/albums/Interlude.html", TextView::npos};
+ REQUIRE(ctv2 == ctv3);
+};
+
+TEST_CASE("TextView Formatting", "[libswoc][TextView]") {
+ TextView a("01234567");
+ {
+ std::ostringstream buff;
+ buff << '|' << a << '|';
+ REQUIRE(buff.str() == "|01234567|");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(5) << a << '|';
+ REQUIRE(buff.str() == "|01234567|");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(12) << a << '|';
+ REQUIRE(buff.str() == "| 01234567|");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(12) << std::right << a << '|';
+ REQUIRE(buff.str() == "| 01234567|");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(12) << std::left << a << '|';
+ REQUIRE(buff.str() == "|01234567 |");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(12) << std::right << std::setfill('_') << a << '|';
+ REQUIRE(buff.str() == "|____01234567|");
+ }
+ {
+ std::ostringstream buff;
+ buff << '|' << std::setw(12) << std::left << std::setfill('_') << a << '|';
+ REQUIRE(buff.str() == "|01234567____|");
+ }
+}
+
+TEST_CASE("TextView Conversions", "[libswoc][TextView]") {
+ TextView n = " 956783";
+ TextView n2 = n;
+ TextView n3 = "031";
+ TextView n4 = "13f8q";
+ TextView n5 = "0x13f8";
+ TextView n6 = "0X13f8";
+ TextView n7 = "-2345679";
+ TextView n8 = "+2345679";
+ TextView x;
+ n2.ltrim_if(&isspace);
+
+ REQUIRE(956783 == svtoi(n));
+ REQUIRE(956783 == svtoi(n2));
+ REQUIRE(956783 == svtoi(n2, &x));
+ REQUIRE(x.data() == n2.data());
+ REQUIRE(x.size() == n2.size());
+ REQUIRE(0x13f8 == svtoi(n4, &x, 16));
+ REQUIRE(x == "13f8");
+ REQUIRE(0x13f8 == svtoi(n5));
+ REQUIRE(0x13f8 == svtoi(n6));
+
+ REQUIRE(25 == svtoi(n3));
+ REQUIRE(31 == svtoi(n3, nullptr, 10));
+
+ REQUIRE(-2345679 == svtoi(n7));
+ REQUIRE(-2345679 == svtoi(n7, &x));
+ REQUIRE(x == n7);
+ REQUIRE(2345679 == svtoi(n8));
+ REQUIRE(2345679 == svtoi(n8, &x));
+ REQUIRE(x == n8);
+ REQUIRE(0b10111 == svtoi("0b10111"_tv));
+
+ x = n4;
+ REQUIRE(13 == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() + 2 == n4.size());
+ x = n4;
+ REQUIRE(0x13f8 == swoc::svto_radix<16>(x));
+ REQUIRE(x.size() + 4 == n4.size());
+ x = n4;
+ REQUIRE(7 == swoc::svto_radix<4>(x));
+ REQUIRE(x.size() + 2 == n4.size());
+ x = n3;
+ REQUIRE(31 == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() == 0);
+ x = n3;
+ REQUIRE(25 == swoc::svto_radix<8>(x));
+ REQUIRE(x.size() == 0);
+
+ // Check overflow conditions
+ static constexpr auto UMAX = std::numeric_limits<uintmax_t>::max();
+ static constexpr auto IMAX = std::numeric_limits<intmax_t>::max();
+ static constexpr auto IMIN = std::numeric_limits<intmax_t>::min();
+
+ // One less than max.
+ x.assign("18446744073709551614");
+ REQUIRE(UMAX - 1 == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() == 0);
+
+ // Exactly max.
+ x.assign("18446744073709551615");
+ REQUIRE(UMAX == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() == 0);
+ x.assign("18446744073709551615");
+ CHECK(UMAX == svtou(x));
+
+ // Should overflow and clamp.
+ x.assign("18446744073709551616");
+ REQUIRE(UMAX == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() == 0);
+
+ // Even more digits.
+ x.assign("18446744073709551616123456789");
+ REQUIRE(UMAX == swoc::svto_radix<10>(x));
+ REQUIRE(x.size() == 0);
+
+ // This is a special value - where N*10 > N while also overflowing. The final "1" causes this.
+ // Be sure overflow is detected.
+ x.assign("27381885734412615681");
+ REQUIRE(UMAX == swoc::svto_radix<10>(x));
+
+ x.assign("9223372036854775807");
+ CHECK(svtou(x) == IMAX);
+ CHECK(svtoi(x) == IMAX);
+ x.assign("9223372036854775808");
+ CHECK(svtou(x) == uintmax_t(IMAX) + 1);
+ CHECK(svtoi(x) == IMAX);
+
+ x.assign("-9223372036854775807");
+ CHECK(svtoi(x) == IMIN + 1);
+ x.assign("-9223372036854775808");
+ CHECK(svtoi(x) == IMIN);
+ x.assign("-9223372036854775809");
+ CHECK(svtoi(x) == IMIN);
+
+ // floating point is never exact, so "good enough" is all that iisnts measureable. This checks the
+ // value is within one epsilon (minimum change possible) of the compiler generated value.
+ auto fcmp = [](double lhs, double rhs) {
+ double tolerance = std::max({1.0, std::fabs(lhs), std::fabs(rhs)}) * std::numeric_limits<double>::epsilon();
+ return std::fabs(lhs - rhs) <= tolerance;
+ };
+
+ REQUIRE(1.0 == swoc::svtod("1.0"));
+ REQUIRE(2.0 == swoc::svtod("2.0"));
+ REQUIRE(true == fcmp(0.1, swoc::svtod("0.1")));
+ REQUIRE(true == fcmp(0.1, swoc::svtod(".1")));
+ REQUIRE(true == fcmp(0.02, swoc::svtod("0.02")));
+ REQUIRE(true == fcmp(2.718281828, swoc::svtod("2.718281828")));
+ REQUIRE(true == fcmp(-2.718281828, swoc::svtod("-2.718281828")));
+ REQUIRE(true == fcmp(2.718281828, swoc::svtod("+2.718281828")));
+ REQUIRE(true == fcmp(0.004, swoc::svtod("4e-3")));
+ REQUIRE(true == fcmp(4e-3, swoc::svtod("4e-3")));
+ REQUIRE(true == fcmp(500000, swoc::svtod("5e5")));
+ REQUIRE(true == fcmp(5e5, swoc::svtod("5e+5")));
+ REQUIRE(true == fcmp(678900, swoc::svtod("6.789E5")));
+ REQUIRE(true == fcmp(6.789e5, swoc::svtod("6.789E+5")));
+}
+
+TEST_CASE("TransformView", "[libswoc][TransformView]") {
+ std::string_view source{"Evil Dave Rulz"};
+ std::string_view rot13("Rivy Qnir Ehym");
+
+ // Because, sadly, the type of @c tolower varies between compilers since @c noexcept
+ // is part of the signature in C++17.
+ swoc::TransformView<decltype(&tolower), std::string_view> xv1(&tolower, source);
+ auto xv2 = swoc::transform_view_of(&tolower, source);
+ // Rot13 transform
+ auto rotter = swoc::transform_view_of(
+ [](char c) { return isalpha(c) ? c > 'Z' ? ('a' + ((c - 'a' + 13) % 26)) : ('A' + ((c - 'A' + 13) % 26)) : c; }, source);
+ auto identity = swoc::transform_view_of(source);
+
+ TextView tv{source};
+
+ REQUIRE(xv1 == xv2);
+
+ // Do this with inline post-fix increments.
+ bool match_p = true;
+ while (xv1) {
+ if (*xv1++ != tolower(*tv++)) {
+ match_p = false;
+ break;
+ }
+ }
+ REQUIRE(match_p);
+ REQUIRE(xv1 != xv2);
+
+ // Do this one with separate pre-fix increments.
+ tv = source;
+ match_p = true;
+ while (xv2) {
+ if (*xv2 != tolower(*tv)) {
+ match_p = false;
+ break;
+ }
+ ++xv2;
+ ++tv;
+ }
+
+ REQUIRE(match_p);
+
+ std::string check;
+ std::copy(rotter.begin(), rotter.end(), std::back_inserter(check));
+ REQUIRE(check == rot13);
+
+ check.clear();
+ for (auto c : identity) {
+ check.push_back(c);
+ }
+ REQUIRE(check == source);
+
+ check.clear();
+ check.append(rotter.begin(), rotter.end());
+ REQUIRE(check == rot13);
+}
+
+TEST_CASE("TextView compat", "[libswoc][TextView]") {
+ struct Thing {
+ int n = 0;
+ };
+ std::map<TextView, Thing> map;
+ std::unordered_map<TextView, Thing> umap;
+
+ // This isn't rigorous, it's mainly testing compilation.
+ map.insert({"bob"_tv, Thing{2}});
+ map["dave"] = Thing{3};
+ umap.insert({"bob"_tv, Thing{4}});
+ umap["dave"] = Thing{6};
+
+ REQUIRE(map["bob"].n == 2);
+ REQUIRE(umap["dave"].n == 6);
+}
+
+TEST_CASE("TextView tokenizing", "[libswoc][TextView]") {
+ TextView src = "alpha,bravo,,charlie";
+ auto tokens = {"alpha", "bravo", "", "charlie"};
+ for (auto token : tokens) {
+ REQUIRE(src.take_prefix_at(',') == token);
+ }
+}
diff --git a/lib/swoc/unit_tests/test_Vectray.cc b/lib/swoc/unit_tests/test_Vectray.cc
new file mode 100644
index 0000000000..ebe7d34b0d
--- /dev/null
+++ b/lib/swoc/unit_tests/test_Vectray.cc
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ MemSpan unit tests.
+
+*/
+
+#include <iostream>
+#include "swoc/Vectray.h"
+#include "catch.hpp"
+
+using swoc::Vectray;
+
+TEST_CASE("Vectray", "[libswoc][Vectray]") {
+ struct Thing {
+ unsigned n = 56;
+ Thing() = default;
+ Thing(Thing const &that) = default;
+ Thing(Thing &&that) : n(that.n) { that.n = 0; }
+ Thing(unsigned u) : n(u) {}
+ };
+
+ Vectray<Thing, 1> unit_thing;
+ Thing PhysicalThing{0};
+
+ REQUIRE(unit_thing.size() == 0);
+
+ unit_thing.push_back(PhysicalThing); // Copy construct
+ REQUIRE(unit_thing.size() == 1);
+ unit_thing.push_back(Thing{1});
+ REQUIRE(unit_thing.size() == 2);
+ unit_thing.push_back(Thing{2});
+ REQUIRE(unit_thing.size() == 3);
+
+ // Check via indexed access.
+ for (unsigned idx = 0; idx < unit_thing.size(); ++idx) {
+ REQUIRE(unit_thing[idx].n == idx);
+ }
+
+ // Check via container access.
+ unsigned n = 0;
+ for (auto const &thing : unit_thing) {
+ REQUIRE(thing.n == n);
+ ++n;
+ }
+ REQUIRE(n == unit_thing.size());
+
+ Thing tmp{99};
+ unit_thing.push_back(std::move(tmp));
+ REQUIRE(unit_thing[3].n == 99);
+ REQUIRE(tmp.n == 0);
+ PhysicalThing.n = 101;
+ unit_thing.push_back(PhysicalThing);
+ REQUIRE(unit_thing.back().n == 101);
+ REQUIRE(PhysicalThing.n == 101);
+}
+
+TEST_CASE("Vectray Destructor", "[libswoc][Vectray]") {
+ int count = 0;
+ struct Q {
+ int &count_;
+ Q(int &count) : count_(count) {}
+ ~Q() { ++count_; }
+ };
+
+ {
+ Vectray<Q, 1> v1;
+ v1.emplace_back(count);
+ }
+ REQUIRE(count == 1);
+
+ count = 0;
+ { // force use of dynamic memory.
+ Vectray<Q, 1> v2;
+ v2.emplace_back(count);
+ v2.emplace_back(count);
+ v2.emplace_back(count);
+ }
+ // Hard to get an exact cound because of std::vector resizes.
+ // But first object should be at least double deleted because of transfer.
+ REQUIRE(count >= 4);
+}
diff --git a/lib/swoc/unit_tests/test_bw_format.cc b/lib/swoc/unit_tests/test_bw_format.cc
new file mode 100644
index 0000000000..4f98abc66e
--- /dev/null
+++ b/lib/swoc/unit_tests/test_bw_format.cc
@@ -0,0 +1,694 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+
+ Unit tests for BufferFormat and bwprint.
+ */
+
+#include <chrono>
+#include <iostream>
+#include <variant>
+
+#include <netinet/in.h>
+
+#include "swoc/MemSpan.h"
+#include "swoc/BufferWriter.h"
+#include "swoc/bwf_std.h"
+#include "swoc/bwf_ex.h"
+
+#include "catch.hpp"
+
+using namespace std::literals;
+using namespace swoc::literals;
+using swoc::TextView;
+using swoc::bwprint, swoc::bwappend;
+
+TEST_CASE("Buffer Writer << operator", "[bufferwriter][stream]") {
+ swoc::LocalBufferWriter<50> bw;
+
+ bw << "The" << ' ' << "quick" << ' ' << "brown fox";
+
+ REQUIRE(bw.view() == "The quick brown fox");
+
+ bw.clear();
+ bw << "x=" << bw.capacity();
+ REQUIRE(bw.view() == "x=50");
+}
+
+TEST_CASE("bwprint basics", "[bwprint]") {
+ swoc::LocalBufferWriter<256> bw;
+ std::string_view fmt1{"Some text"sv};
+ swoc::bwf::Format fmt2("left >{0:<9}< right >{0:>9}< center >{0:^9}<");
+ std::string_view text{"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
+ static const swoc::bwf::Format bad_arg_fmt{"{{BAD_ARG_INDEX:{} of {}}}"};
+
+ bw.print(fmt1);
+ REQUIRE(bw.view() == fmt1);
+ bw.clear().print("Some text"); // check that a literal string works as expected.
+ REQUIRE(bw.view() == fmt1);
+ bw.clear().print("Some text"_tv); // check that a literal TextView works.
+ REQUIRE(bw.view() == fmt1);
+ bw.clear().print("Arg {}", 1);
+ REQUIRE(bw.view() == "Arg 1");
+ bw.clear().print("arg 1 {1} and 2 {2} and 0 {0}", "zero", "one", "two");
+ REQUIRE(bw.view() == "arg 1 one and 2 two and 0 zero");
+ bw.clear().print("args {2}{0}{1}", "zero", "one", "two");
+ REQUIRE(bw.view() == "args twozeroone");
+ bw.clear().print("left |{:<10}|", "text");
+ REQUIRE(bw.view() == "left |text |");
+ bw.clear().print("right |{:>10}|", "text");
+ REQUIRE(bw.view() == "right | text|");
+ bw.clear().print("right |{:.>10}|", "text");
+ REQUIRE(bw.view() == "right |......text|");
+ bw.clear().print("center |{:.^10}|", "text");
+ REQUIRE(bw.view() == "center |...text...|");
+ bw.clear().print("center |{:.^11}|", "text");
+ REQUIRE(bw.view() == "center |...text....|");
+ bw.clear().print("center |{:^^10}|", "text");
+ REQUIRE(bw.view() == "center |^^^text^^^|");
+ bw.clear().print("center |{:%3A^10}|", "text");
+ REQUIRE(bw.view() == "center |:::text:::|");
+ bw.clear().print("left >{0:<9}< right >{0:>9}< center >{0:^9}<", 956);
+ REQUIRE(bw.view() == "left >956 < right > 956< center > 956 <");
+
+ bw.clear().print("Format |{:>#010x}|", -956);
+ REQUIRE(bw.view() == "Format |0000-0x3bc|");
+ bw.clear().print("Format |{:<#010x}|", -956);
+ REQUIRE(bw.view() == "Format |-0x3bc0000|");
+ bw.clear().print("Format |{:#010x}|", -956);
+ REQUIRE(bw.view() == "Format |-0x00003bc|");
+
+ bw.clear().print("{{BAD_ARG_INDEX:{} of {}}}", 17, 23);
+ REQUIRE(bw.view() == "{BAD_ARG_INDEX:17 of 23}");
+
+ bw.clear().print("Arg {0} Arg {3}", 0, 1);
+ REQUIRE(bw.view() == "Arg 0 Arg {BAD_ARG_INDEX:3 of 2}");
+
+ bw.clear().print("{{stuff}} Arg {0} Arg {}", 0, 1, 2);
+ REQUIRE(bw.view() == "{stuff} Arg 0 Arg 0");
+ bw.clear().print("{{stuff}} Arg {0} Arg {} {}", 0, 1, 2);
+ REQUIRE(bw.view() == "{stuff} Arg 0 Arg 0 1");
+ bw.clear();
+ bw.print("Arg {0} Arg {} and {{stuff}}", 3, 4);
+ REQUIRE(bw.view() == "Arg 3 Arg 3 and {stuff}");
+ bw.clear().print("Arg {{{0}}} Arg {} and {{stuff}}", 5, 6);
+ REQUIRE(bw.view() == "Arg {5} Arg 5 and {stuff}");
+ bw.clear().print("Arg {{{0}}} Arg {} {1} {} {0} and {{stuff}}", 5, 6);
+ REQUIRE(bw.view() == "Arg {5} Arg 5 6 6 5 and {stuff}");
+ bw.clear();
+ bw.print("Arg {0} Arg {{}}{{}} {} and {} {{stuff}}", 7, 8);
+ REQUIRE(bw.view() == "Arg 7 Arg {}{} 7 and 8 {stuff}");
+ bw.clear();
+ bw.print("Arg {} Arg {{{{}}}} {} {1} {0}", 9, 10);
+ REQUIRE(bw.view() == "Arg 9 Arg {{}} 10 10 9");
+
+ bw.clear();
+ bw.print("Arg {} Arg {{{{}}}} {}", 9, 10);
+ REQUIRE(bw.view() == "Arg 9 Arg {{}} 10");
+ bw.clear().print(bad_arg_fmt, 17, 23);
+ REQUIRE(bw.view() == "{BAD_ARG_INDEX:17 of 23}");
+
+ bw.clear().print("{leif}");
+ REQUIRE(bw.view() == "{~leif~}"); // expected to be missing.
+
+ bw.clear().print(fmt2, 956);
+ REQUIRE(bw.view() == "left >956 < right > 956< center > 956 <");
+
+ // Check leading space printing.
+ bw.clear().print(" {}", fmt1);
+ REQUIRE(bw.view() == " Some text");
+
+ std::string_view fmt_sv = "Answer: \"{}\" Surprise!";
+ std::string_view answer = "Evil Dave";
+ bw.clear().print(fmt_sv, answer);
+ REQUIRE(bw.view().size() == fmt_sv.size() + answer.size() - 2);
+}
+
+TEST_CASE("BWFormat numerics", "[bwprint][bwformat]") {
+ swoc::LocalBufferWriter<256> bw;
+
+ void *ptr = reinterpret_cast<void *>(0XBADD0956);
+ bw.clear();
+ bw.print("{}", ptr);
+ REQUIRE(bw.view() == "0xbadd0956");
+ bw.clear();
+ bw.print("{:X}", ptr);
+ REQUIRE(bw.view() == "0XBADD0956");
+ int *int_ptr = static_cast<int *>(ptr);
+ bw.clear();
+ bw.print("{}", int_ptr);
+ REQUIRE(bw.view() == "0xbadd0956");
+ char char_ptr[] = "delain";
+ bw.clear();
+ bw.print("{:x}", static_cast<char *>(ptr));
+ REQUIRE(bw.view() == "0xbadd0956");
+ bw.clear();
+ bw.print("{}", char_ptr);
+ REQUIRE(bw.view() == "delain");
+
+ swoc::MemSpan span{ptr, 0x200};
+ bw.clear().print("{}", span);
+ REQUIRE(bw.view() == "0x200@0xbadd0956");
+
+ swoc::MemSpan cspan{char_ptr, 6};
+ bw.clear().print("{:x}", cspan);
+ REQUIRE(bw.view() == "64 65 6c 61 69 6e");
+ bw.clear().print("{:#x}", cspan);
+ REQUIRE(bw.view() == "0x64 0x65 0x6c 0x61 0x69 0x6e");
+ bw.clear().print("{:#.2x}", cspan);
+ REQUIRE(bw.view() == "0x6465 0x6c61 0x696e");
+ bw.clear().print("{:x}", cspan.rebind());
+ REQUIRE(bw.view() == "64656c61696e");
+
+ TextView sv{"abc123"};
+ bw.clear();
+ bw.print("{}", sv);
+ REQUIRE(bw.view() == sv);
+ bw.clear();
+ bw.print("{:x}", sv);
+ REQUIRE(bw.view() == "616263313233");
+ bw.clear();
+ bw.print("{:#x}", sv);
+ REQUIRE(bw.view() == "0x616263313233");
+ bw.clear();
+ bw.print("|{:16x}|", sv);
+ REQUIRE(bw.view() == "|616263313233 |");
+ bw.clear();
+ bw.print("|{:>16x}|", sv);
+ REQUIRE(bw.view() == "| 616263313233|");
+ bw.clear().print("|{:^16x}|", sv);
+ REQUIRE(bw.view() == "| 616263313233 |");
+ bw.clear().print("|{:>16.2x}|", sv);
+ REQUIRE(bw.view() == "| 6162|");
+
+ // Substrings by argument adjustment.
+ bw.clear().print("|{:<0,7x}|", sv.prefix(4));
+ REQUIRE(bw.view() == "|6162633|");
+ bw.clear().print("|{:<5,7x}|", sv.prefix(2));
+ REQUIRE(bw.view() == "|6162 |");
+ bw.clear().print("|{:<5,7x}|", sv.prefix(3));
+ REQUIRE(bw.view() == "|616263|");
+ bw.clear().print("|{:<7x}|", sv.prefix(3));
+ REQUIRE(bw.view() == "|616263 |");
+
+ // Substrings by precision - should be same output.
+ bw.clear().print("|{:<0.4,7x}|", sv);
+ REQUIRE(bw.view() == "|6162633|");
+ bw.clear().print("|{:<5.2,7x}|", sv);
+ REQUIRE(bw.view() == "|6162 |");
+ bw.clear().print("|{:<5.3,7x}|", sv);
+ REQUIRE(bw.view() == "|616263|");
+ bw.clear().print("|{:<7.3x}|", sv);
+ REQUIRE(bw.view() == "|616263 |");
+
+ bw.clear();
+ bw.print("|{}|", true);
+ REQUIRE(bw.view() == "|1|");
+ bw.clear();
+ bw.print("|{}|", false);
+ REQUIRE(bw.view() == "|0|");
+ bw.clear();
+ bw.print("|{:s}|", true);
+ REQUIRE(bw.view() == "|true|");
+ bw.clear();
+ bw.print("|{:S}|", false);
+ REQUIRE(bw.view() == "|FALSE|");
+ bw.clear();
+ bw.print("|{:>9s}|", false);
+ REQUIRE(bw.view() == "| false|");
+ bw.clear();
+ bw.print("|{:^10s}|", true);
+ REQUIRE(bw.view() == "| true |");
+
+ // Test clipping a bit.
+ swoc::LocalBufferWriter<20> bw20;
+ bw20.print("0123456789abc|{:^10s}|", true);
+ REQUIRE(bw20.view() == "0123456789abc| tru");
+ bw20.clear();
+ bw20.print("012345|{:^10s}|6789abc", true);
+ REQUIRE(bw20.view() == "012345| true |67");
+
+ bw.clear().print("Char '{}'", 'a');
+ REQUIRE(bw.view() == "Char 'a'");
+ bw.clear().print("Byte '{}'", uint8_t{'a'});
+ REQUIRE(bw.view() == "Byte '97'");
+
+ SECTION("Hexadecimal buffers") {
+ swoc::MemSpan<void const> cvs{"Evil Dave Rulz"_tv}; // TextView intermediate keeps nul byte out.
+ TextView const edr_in_hex{"4576696c20446176652052756c7a"};
+ bw.clear().format(swoc::bwf::Spec(":x"), cvs);
+ REQUIRE(bw.view() == edr_in_hex);
+ bw.clear().format(swoc::bwf::Spec::DEFAULT, swoc::bwf::UnHex(edr_in_hex));
+ REQUIRE(bw.view() == "Evil Dave Rulz");
+ bw.clear().format(swoc::bwf::Spec::DEFAULT, swoc::bwf::UnHex("112233445566778800"));
+ REQUIRE(memcmp(bw.view(), "\x11\x22\x33\x44\x55\x66\x77\x88\x00"_tv) == 0);
+ // Check if max width in the spec works - should leave bytes from the previous.
+ bw.clear().format(swoc::bwf::Spec{":,2"}, swoc::bwf::UnHex("deadbeef"));
+ REQUIRE(memcmp(TextView(bw.data(), 4), "\xde\xad\x33\x44"_tv) == 0);
+ std::string text, hex;
+ bwprint(hex, "{:x}", cvs);
+ bwprint(text, "{}", swoc::bwf::UnHex(edr_in_hex));
+ REQUIRE(hex == edr_in_hex);
+ REQUIRE(text == TextView(cvs.rebind<char const>()));
+ }
+}
+
+TEST_CASE("bwstring", "[bwprint][bwappend][bwstring]") {
+ std::string s;
+ swoc::TextView fmt("{} -- {}");
+ std::string_view text{"e99a18c428cb38d5f260853678922e03"};
+
+ bwprint(s, fmt, "string", 956);
+ REQUIRE(s.size() == 13);
+ REQUIRE(s == "string -- 956");
+
+ bwprint(s, fmt, 99999, text);
+ REQUIRE(s == "99999 -- e99a18c428cb38d5f260853678922e03");
+
+ bwprint(s, "{} .. |{:,20}|", 32767, text);
+ REQUIRE(s == "32767 .. |e99a18c428cb38d5f260|");
+
+ swoc::LocalBufferWriter<128> bw;
+ char buff[128];
+ snprintf(buff, sizeof(buff), "|%s|", bw.print("Deep Silent Complete by {}\0", "Nightwish"sv).data());
+ REQUIRE(std::string_view(buff) == "|Deep Silent Complete by Nightwish|");
+ snprintf(buff, sizeof(buff), "|%s|", bw.clear().print("Deep Silent Complete by {}\0elided junk", "Nightwish"sv).data());
+ REQUIRE(std::string_view(buff) == "|Deep Silent Complete by Nightwish|");
+
+ // Special tests for clang analyzer failures - special asserts are needed to make it happy but
+ // those can break functionality.
+ fmt = "Did you know? {}{} is {}"sv;
+ s.resize(0);
+ bwprint(s, fmt, "Lady "sv, "Persia"sv, "not mean");
+ REQUIRE(s == "Did you know? Lady Persia is not mean");
+ s.resize(0);
+ bwprint(s, fmt, ""sv, "Phil", "correct");
+ REQUIRE(s == "Did you know? Phil is correct");
+ s.resize(0);
+ bwprint(s, fmt, std::string_view(), "Leif", "confused");
+ REQUIRE(s == "Did you know? Leif is confused");
+
+ {
+ std::string out;
+ bwprint(out, fmt, ""sv, "Phil", "correct");
+ REQUIRE(out == "Did you know? Phil is correct");
+ }
+ {
+ std::string out;
+ bwprint(out, fmt, std::string_view(), "Leif", "confused");
+ REQUIRE(out == "Did you know? Leif is confused");
+ }
+
+ char const *null_string{nullptr};
+ bwprint(s, "Null {0:x}.{0}", null_string);
+ REQUIRE(s == "Null 0x0.");
+ bwprint(s, "Null {0:X}.{0}", nullptr);
+ REQUIRE(s == "Null 0X0.");
+ bwprint(s, "Null {0:p}.{0:P}.{0:s}.{0:S}", null_string);
+ REQUIRE(s == "Null 0x0.0X0.null.NULL");
+
+ {
+ std::string x;
+ bwappend(x, "Phil");
+ REQUIRE(x == "Phil");
+ bwappend(x, " is {} most of the time", "correct"_tv);
+ REQUIRE(x == "Phil is correct most of the time");
+ x.resize(0); // try it with already sufficient capacity.
+ bwappend(x, "Dave");
+ REQUIRE(x == "Dave");
+ bwappend(x, " is {} some of the time", "correct"_tv);
+ REQUIRE(x == "Dave is correct some of the time");
+ }
+}
+
+TEST_CASE("BWFormat integral", "[bwprint][bwformat]") {
+ swoc::LocalBufferWriter<256> bw;
+ swoc::bwf::Spec spec;
+ uint32_t num = 30;
+ int num_neg = -30;
+
+ // basic
+ bwformat(bw, spec, num);
+ REQUIRE(bw.view() == "30");
+ bw.clear();
+ bwformat(bw, spec, num_neg);
+ REQUIRE(bw.view() == "-30");
+ bw.clear();
+
+ // radix
+ swoc::bwf::Spec spec_hex;
+ spec_hex._radix_lead_p = true;
+ spec_hex._type = 'x';
+ bwformat(bw, spec_hex, num);
+ REQUIRE(bw.view() == "0x1e");
+ bw.clear();
+
+ swoc::bwf::Spec spec_dec;
+ spec_dec._type = '0';
+ bwformat(bw, spec_dec, num);
+ REQUIRE(bw.view() == "30");
+ bw.clear();
+
+ swoc::bwf::Spec spec_bin;
+ spec_bin._radix_lead_p = true;
+ spec_bin._type = 'b';
+ bwformat(bw, spec_bin, num);
+ REQUIRE(bw.view() == "0b11110");
+ bw.clear();
+
+ int one = 1;
+ int two = 2;
+ int three_n = -3;
+ // alignment
+ swoc::bwf::Spec left;
+ left._align = swoc::bwf::Spec::Align::LEFT;
+ left._min = 5;
+ swoc::bwf::Spec right;
+ right._align = swoc::bwf::Spec::Align::RIGHT;
+ right._min = 5;
+ swoc::bwf::Spec center;
+ center._align = swoc::bwf::Spec::Align::CENTER;
+ center._min = 5;
+
+ bwformat(bw, left, one);
+ bwformat(bw, right, two);
+ REQUIRE(bw.view() == "1 2");
+ bwformat(bw, right, two);
+ REQUIRE(bw.view() == "1 2 2");
+ bwformat(bw, center, three_n);
+ REQUIRE(bw.view() == "1 2 2 -3 ");
+
+ std::atomic<int> ax{0};
+ bw.clear().print("ax == {}", ax);
+ REQUIRE(bw.view() == "ax == 0");
+ ++ax;
+ bw.clear().print("ax == {}", ax);
+ REQUIRE(bw.view() == "ax == 1");
+}
+
+TEST_CASE("BWFormat floating", "[bwprint][bwformat]") {
+ swoc::LocalBufferWriter<256> bw;
+ swoc::bwf::Spec spec;
+
+ bw.clear();
+ bw.print("{}", 3.14);
+ REQUIRE(bw.view() == "3.14");
+ bw.clear();
+ bw.print("{} {:.2} {:.0} ", 32.7, 32.7, 32.7);
+ REQUIRE(bw.view() == "32.70 32.70 32 ");
+ bw.clear();
+ bw.print("{} neg {:.3}", -123.2, -123.2);
+ REQUIRE(bw.view() == "-123.20 neg -123.200");
+ bw.clear();
+ bw.print("zero {} quarter {} half {} 3/4 {}", 0, 0.25, 0.50, 0.75);
+ REQUIRE(bw.view() == "zero 0 quarter 0.25 half 0.50 3/4 0.75");
+ bw.clear();
+ bw.print("long {:.11}", 64.9);
+ REQUIRE(bw.view() == "long 64.90000000000");
+ bw.clear();
+
+ double n = 180.278;
+ double neg = -238.47;
+ bwformat(bw, spec, n);
+ REQUIRE(bw.view() == "180.28");
+ bw.clear();
+ bwformat(bw, spec, neg);
+ REQUIRE(bw.view() == "-238.47");
+ bw.clear();
+
+ spec._prec = 5;
+ bwformat(bw, spec, n);
+ REQUIRE(bw.view() == "180.27800");
+ bw.clear();
+ bwformat(bw, spec, neg);
+ REQUIRE(bw.view() == "-238.47000");
+ bw.clear();
+
+ float f = 1234;
+ float fneg = -1;
+ bwformat(bw, spec, f);
+ REQUIRE(bw.view() == "1234");
+ bw.clear();
+ bwformat(bw, spec, fneg);
+ REQUIRE(bw.view() == "-1");
+ bw.clear();
+ f = 1234.5667;
+ spec._prec = 4;
+ bwformat(bw, spec, f);
+ REQUIRE(bw.view() == "1234.5667");
+ bw.clear();
+
+ bw << 1234 << .567;
+ REQUIRE(bw.view() == "12340.57");
+ bw.clear();
+ bw << f;
+ REQUIRE(bw.view() == "1234.57");
+ bw.clear();
+ bw << n;
+ REQUIRE(bw.view() == "180.28");
+ bw.clear();
+ bw << f << n;
+ REQUIRE(bw.view() == "1234.57180.28");
+ bw.clear();
+
+ double edge = 0.345;
+ spec._prec = 3;
+ bwformat(bw, spec, edge);
+ REQUIRE(bw.view() == "0.345");
+ bw.clear();
+ edge = .1234;
+ bwformat(bw, spec, edge);
+ REQUIRE(bw.view() == "0.123");
+ bw.clear();
+ edge = 1.0;
+ bwformat(bw, spec, edge);
+ REQUIRE(bw.view() == "1");
+ bw.clear();
+
+ // alignment
+ double first = 1.23;
+ double second = 2.35;
+ double third = -3.5;
+ swoc::bwf::Spec left;
+ left._align = swoc::bwf::Spec::Align::LEFT;
+ left._min = 5;
+ swoc::bwf::Spec right;
+ right._align = swoc::bwf::Spec::Align::RIGHT;
+ right._min = 5;
+ swoc::bwf::Spec center;
+ center._align = swoc::bwf::Spec::Align::CENTER;
+ center._min = 5;
+
+ bwformat(bw, left, first);
+ bwformat(bw, right, second);
+ REQUIRE(bw.view() == "1.23 2.35");
+ bwformat(bw, right, second);
+ REQUIRE(bw.view() == "1.23 2.35 2.35");
+ bwformat(bw, center, third);
+ REQUIRE(bw.view() == "1.23 2.35 2.35-3.50");
+ bw.clear();
+
+ double over = 1.4444444;
+ swoc::bwf::Spec over_min;
+ over_min._prec = 7;
+ over_min._min = 5;
+ bwformat(bw, over_min, over);
+ REQUIRE(bw.view() == "1.4444444");
+ bw.clear();
+
+ // Edge
+ bw.print("{}", (1.0 / 0.0));
+ REQUIRE(bw.view() == "Inf");
+ bw.clear();
+
+ double inf = std::numeric_limits<double>::infinity();
+ bw.print(" {} ", inf);
+ REQUIRE(bw.view() == " Inf ");
+ bw.clear();
+
+ double nan_1 = std::nan("1");
+ bw.print("{} {}", nan_1, nan_1);
+ REQUIRE(bw.view() == "NaN NaN");
+ bw.clear();
+
+ double z = 0.0;
+ bw.print("{} ", z);
+ REQUIRE(bw.view() == "0 ");
+ bw.clear();
+}
+
+TEST_CASE("bwstring std formats", "[libswoc][bwprint]") {
+ std::string_view text{"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
+ swoc::LocalBufferWriter<120> w;
+
+ w.print("{}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES: Permission denied [13]"sv);
+ w.clear().print("{}", swoc::bwf::Errno(134));
+ REQUIRE(w.view().substr(0, 22) == "Unknown: Unknown error"sv);
+ w.clear().print("{:s}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES: Permission denied"sv);
+ w.clear().print("{:S}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES: Permission denied"sv);
+ w.clear().print("{:s:s}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES"sv);
+ w.clear().print("{:s:l}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "Permission denied"sv);
+ w.clear().print("{:s:sl}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES: Permission denied"sv);
+ w.clear().print("{:d}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "[13]"sv);
+ w.clear().print("{:g}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES: Permission denied [13]"sv);
+ w.clear().print("{:g:s}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES [13]"sv);
+ w.clear().print("{::s}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "EACCES [13]"sv);
+ w.clear().print("{::l}", swoc::bwf::Errno(13));
+ REQUIRE(w.view() == "Permission denied [13]"sv);
+
+ time_t t = 1528484137;
+ // default is GMT
+ w.clear().print("{} is {}", t, swoc::bwf::Date(t));
+ REQUIRE(w.view() == "1528484137 is 2018 Jun 08 18:55:37");
+ w.clear().print("{} is {}", t, swoc::bwf::Date(t, "%a, %d %b %Y at %H.%M.%S"));
+ REQUIRE(w.view() == "1528484137 is Fri, 08 Jun 2018 at 18.55.37");
+ // OK to be explicit
+ w.clear().print("{} is {::gmt}", t, swoc::bwf::Date(t));
+ REQUIRE(w.view() == "1528484137 is 2018 Jun 08 18:55:37");
+ w.clear().print("{} is {::gmt}", t, swoc::bwf::Date(t, "%a, %d %b %Y at %H.%M.%S"));
+ REQUIRE(w.view() == "1528484137 is Fri, 08 Jun 2018 at 18.55.37");
+ // Local time - set it to something specific or the test will be geographically sensitive.
+ setenv("TZ", "CST6", 1);
+ tzset();
+ w.clear().print("{} is {::local}", t, swoc::bwf::Date(t));
+ REQUIRE(w.view() == "1528484137 is 2018 Jun 08 12:55:37");
+ w.clear().print("{} is {::local}", t, swoc::bwf::Date(t, "%a, %d %b %Y at %H.%M.%S"));
+ REQUIRE(w.view() == "1528484137 is Fri, 08 Jun 2018 at 12.55.37");
+
+ unsigned v = htonl(0xdeadbeef);
+ w.clear().print("{}", swoc::bwf::As_Hex(v));
+ REQUIRE(w.view() == "deadbeef");
+ w.clear().print("{:x}", swoc::bwf::As_Hex(v));
+ REQUIRE(w.view() == "deadbeef");
+ w.clear().print("{:X}", swoc::bwf::As_Hex(v));
+ REQUIRE(w.view() == "DEADBEEF");
+ w.clear().print("{:#X}", swoc::bwf::As_Hex(v));
+ REQUIRE(w.view() == "0XDEADBEEF");
+ w.clear().print("{} bytes {} digits {}", sizeof(double), std::numeric_limits<double>::digits10, swoc::bwf::As_Hex(2.718281828));
+ REQUIRE(w.view() == "8 bytes 15 digits 9b91048b0abf0540");
+
+ // Verify these compile and run, not really much hope to check output.
+ w.clear().print("|{}| |{}|", swoc::bwf::Date(), swoc::bwf::Date("%a, %d %b %Y"));
+
+ w.clear().print("name = {}", swoc::bwf::FirstOf("Persia"));
+ REQUIRE(w.view() == "name = Persia");
+ w.clear().print("name = {}", swoc::bwf::FirstOf("Persia", "Evil Dave"));
+ REQUIRE(w.view() == "name = Persia");
+ w.clear().print("name = {}", swoc::bwf::FirstOf("", "Evil Dave"));
+ REQUIRE(w.view() == "name = Evil Dave");
+ w.clear().print("name = {}", swoc::bwf::FirstOf(nullptr, "Evil Dave"));
+ REQUIRE(w.view() == "name = Evil Dave");
+ w.clear().print("name = {}", swoc::bwf::FirstOf("Persia", "Evil Dave", "Leif"));
+ REQUIRE(w.view() == "name = Persia");
+ w.clear().print("name = {}", swoc::bwf::FirstOf("Persia", nullptr, "Leif"));
+ REQUIRE(w.view() == "name = Persia");
+ w.clear().print("name = {}", swoc::bwf::FirstOf("", nullptr, "Leif"));
+ REQUIRE(w.view() == "name = Leif");
+
+ const char *empty{nullptr};
+ std::string s1{"Persia"};
+ std::string_view s2{"Evil Dave"};
+ swoc::TextView s3{"Leif"};
+ w.clear().print("name = {}", swoc::bwf::FirstOf(empty, s3));
+ REQUIRE(w.view() == "name = Leif");
+ w.clear().print("name = {}", swoc::bwf::FirstOf(s2, s3));
+ REQUIRE(w.view() == "name = Evil Dave");
+ w.clear().print("name = {}", swoc::bwf::FirstOf(s1, empty, s2));
+ REQUIRE(w.view() == "name = Persia");
+ w.clear().print("name = {}", swoc::bwf::FirstOf(empty, s2, s1, s3));
+ REQUIRE(w.view() == "name = Evil Dave");
+ w.clear().print("name = {}", swoc::bwf::FirstOf(empty, empty, s3, empty, s2, s1));
+ REQUIRE(w.view() == "name = Leif");
+
+ w.clear().print("Lower - |{:s}|", text);
+ REQUIRE(w.view() == "Lower - |0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz|");
+ w.clear().print("Upper - |{:S}|", text);
+ REQUIRE(w.view() == "Upper - |0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ|");
+
+ w.clear().print("Leading{}{}{}.", swoc::bwf::Optional(" | {} |", s1), swoc::bwf::Optional(" <{}>", empty),
+ swoc::bwf::If(!s3.empty(), " [{}]", s3));
+ REQUIRE(w.view() == "Leading | Persia | [Leif].");
+ // Do it again, but this time as C strings (char * variants).
+ w.clear().print("Leading{}{}{}.", swoc::bwf::Optional(" | {} |", s3.data()), swoc::bwf::Optional(" <{}>", empty),
+ swoc::bwf::If(!s3.empty(), " [{}]", s1.c_str()));
+ REQUIRE(w.view() == "Leading | Leif | [Persia].");
+ // Play with string_view
+ w.clear().print("Clone?{}{}.", swoc::bwf::Optional(" #. {}", s2), swoc::bwf::Optional(" #. {}", s2.data()));
+ REQUIRE(w.view() == "Clone? #. Evil Dave #. Evil Dave.");
+ s2 = "";
+ w.clear().print("Leading{}{}{}", swoc::bwf::If(true, " true"), swoc::bwf::If(false, " false"), swoc::bwf::If(true, " Persia"));
+ REQUIRE(w.view() == "Leading true Persia");
+ // Differentiate because the C string variant will generate output, as it's not nullptr,
+ // but is a pointer to an empty string.
+ w.clear().print("Clone?{}{}.", swoc::bwf::Optional(" 1. {}", s2), swoc::bwf::Optional(" 2. {}", s2.data()));
+ REQUIRE(w.view() == "Clone? 2. .");
+ s2 = std::string_view{};
+ w.clear().print("Clone?{}{}.", swoc::bwf::Optional(" #. {}", s2), swoc::bwf::Optional(" #. {}", s2.data()));
+ REQUIRE(w.view() == "Clone?.");
+
+ SECTION("exception") {
+ std::runtime_error e("Sureness out of bounds");
+ w.clear().print("{}", e);
+ REQUIRE(w.view() == "Exception - Sureness out of bounds");
+ }
+};
+
+// Normally there's no point in running the performance tests, but it's worth keeping the code
+// for when additional testing needs to be done.
+#if 0
+TEST_CASE("bwperf", "[bwprint][performance]")
+{
+ // Force these so I can easily change the set of tests.
+ auto start = std::chrono::high_resolution_clock::now();
+ auto delta = std::chrono::high_resolution_clock::now() - start;
+ constexpr int N_LOOPS = 1000000;
+
+ static constexpr const char * FMT = "Format |{:#010x}| '{}'";
+ static constexpr swoc::TextView fmt{FMT, strlen(FMT)};
+ static constexpr std::string_view text{"e99a18c428cb38d5f260853678922e03"sv};
+ swoc::LocalBufferWriter<256> bw;
+
+ swoc::bwf::Spec spec;
+
+ bw.clear();
+ bw.print(fmt, -956, text);
+ REQUIRE(bw.view() == "Format |-0x00003bc| 'e99a18c428cb38d5f260853678922e03'");
+
+ start = std::chrono::high_resolution_clock::now();
+ for (int i = 0; i < N_LOOPS; ++i) {
+ bw.clear();
+ bw.print(fmt, -956, text);
+ }
+ delta = std::chrono::high_resolution_clock::now() - start;
+ std::cout << "bw.print() " << delta.count() << "ns or " << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count()
+ << "ms" << std::endl;
+
+ swoc::bwf::Format pre_fmt(fmt);
+ start = std::chrono::high_resolution_clock::now();
+ for (int i = 0; i < N_LOOPS; ++i) {
+ bw.clear();
+ bw.print(pre_fmt, -956, text);
+ }
+ delta = std::chrono::high_resolution_clock::now() - start;
+ std::cout << "Preformatted: " << delta.count() << "ns or "
+ << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count() << "ms" << std::endl;
+
+ char buff[256];
+ start = std::chrono::high_resolution_clock::now();
+ for (int i = 0; i < N_LOOPS; ++i) {
+ snprintf(buff, sizeof(buff), "Format |%#0x10| '%.*s'", -956, static_cast<int>(text.size()), text.data());
+ }
+ delta = std::chrono::high_resolution_clock::now() - start;
+ std::cout << "snprint Timing is " << delta.count() << "ns or "
+ << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count() << "ms" << std::endl;
+}
+#endif
diff --git a/lib/swoc/unit_tests/test_ip.cc b/lib/swoc/unit_tests/test_ip.cc
new file mode 100644
index 0000000000..bb7eadfc31
--- /dev/null
+++ b/lib/swoc/unit_tests/test_ip.cc
@@ -0,0 +1,2186 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2014 Network Geographics
+/** @file
+
+ IP address support testing.
+*/
+
+#include "catch.hpp"
+
+#include <set>
+#include <iostream>
+#include <type_traits>
+
+#include "swoc/TextView.h"
+#include "swoc/swoc_ip.h"
+#include "swoc/bwf_ip.h"
+#include "swoc/bwf_std.h"
+#include "swoc/Lexicon.h"
+
+using namespace std::literals;
+using namespace swoc::literals;
+using swoc::TextView;
+using swoc::IPEndpoint;
+
+using swoc::IP4Addr;
+using swoc::IP4Range;
+
+using swoc::IP6Addr;
+using swoc::IP6Range;
+
+using swoc::IPAddr;
+using swoc::IPRange;
+
+using swoc::IPMask;
+
+using swoc::IP4Net;
+using swoc::IP6Net;
+
+using swoc::IPSpace;
+using swoc::IPRangeSet;
+
+namespace {
+std::string bws;
+
+template <typename P>
+void
+dump(IPSpace<P> const &space) {
+ for (auto &&[r, p] : space) {
+ std::cout << bwprint(bws, "{} : {}\n", r, p);
+ }
+}
+} // namespace
+
+TEST_CASE("Basic IP", "[libswoc][ip]") {
+ IPEndpoint ep;
+
+ // Use TextView because string_view(nullptr) fails. Gah.
+ struct ip_parse_spec {
+ TextView hostspec;
+ TextView host;
+ TextView port;
+ TextView rest;
+ };
+
+ constexpr ip_parse_spec names[] = {
+ {{"::"}, {"::"}, {nullptr}, {nullptr}},
+ {{"[::1]:99"}, {"::1"}, {"99"}, {nullptr}},
+ {{"127.0.0.1:8080"}, {"127.0.0.1"}, {"8080"}, {nullptr}},
+ {{"127.0.0.1:8080-Bob"}, {"127.0.0.1"}, {"8080"}, {"-Bob"} },
+ {{"127.0.0.1:"}, {"127.0.0.1"}, {nullptr}, {":"} },
+ {{"foo.example.com"}, {"foo.example.com"}, {nullptr}, {nullptr}},
+ {{"foo.example.com:99"}, {"foo.example.com"}, {"99"}, {nullptr}},
+ {{"ffee::24c3:3349:3cee:0143"}, {"ffee::24c3:3349:3cee:0143"}, {nullptr}, {nullptr}},
+ {{"fe80:88b5:4a:20c:29ff:feae:1c33:8080"}, {"fe80:88b5:4a:20c:29ff:feae:1c33:8080"}, {nullptr}, {nullptr}},
+ {{"[ffee::24c3:3349:3cee:0143]"}, {"ffee::24c3:3349:3cee:0143"}, {nullptr}, {nullptr}},
+ {{"[ffee::24c3:3349:3cee:0143]:80"}, {"ffee::24c3:3349:3cee:0143"}, {"80"}, {nullptr}},
+ {{"[ffee::24c3:3349:3cee:0143]:8080x"}, {"ffee::24c3:3349:3cee:0143"}, {"8080"}, {"x"} }
+ };
+
+ for (auto const &s : names) {
+ std::string_view host, port, rest;
+
+ REQUIRE(IPEndpoint::tokenize(s.hostspec, &host, &port, &rest) == true);
+ REQUIRE(s.host == host);
+ REQUIRE(s.port == port);
+ REQUIRE(s.rest == rest);
+ }
+
+ IP4Addr alpha{"172.96.12.134"};
+ CHECK(alpha == IP4Addr{"172.96.12.134"});
+ CHECK(alpha == IPAddr{IPEndpoint{"172.96.12.134:80"}});
+ CHECK(alpha == IPAddr{IPEndpoint{"172.96.12.134"}});
+ REQUIRE(alpha[1] == 96);
+ REQUIRE(alpha[2] == 12);
+ REQUIRE(alpha[3] == 134);
+
+ // Alternate forms - inet_aton compabitility. Note in truncated forms, the last value is for
+ // all remaining octets, those are not zero filled as in IPv6.
+ CHECK(alpha.load("172.96.12"));
+ REQUIRE(alpha[0] == 172);
+ REQUIRE(alpha[2] == 0);
+ REQUIRE(alpha[3] == 12);
+ CHECK_FALSE(alpha.load("172.96.71117"));
+ CHECK(alpha.load("172.96.3136"));
+ REQUIRE(alpha[0] == 172);
+ REQUIRE(alpha[2] == 0xC);
+ REQUIRE(alpha[3] == 0x40);
+ CHECK(alpha.load("172.12586118"));
+ REQUIRE(alpha[0] == 172);
+ REQUIRE(alpha[1] == 192);
+ REQUIRE(alpha[2] == 12);
+ REQUIRE(alpha[3] == 134);
+ CHECK(alpha.load("172.0xD00D56"));
+ REQUIRE(alpha[0] == 172);
+ REQUIRE(alpha[1] == 0xD0);
+ REQUIRE(alpha[2] == 0x0D);
+ REQUIRE(alpha[3] == 0x56);
+ CHECK_FALSE(alpha.load("192.172.3."));
+ CHECK(alpha.load("192.0xAC.014.135"));
+ REQUIRE(alpha[0] == 192);
+ REQUIRE(alpha[1] == 172);
+ REQUIRE(alpha[2] == 12);
+ REQUIRE(alpha[3] == 135);
+
+ CHECK(IP6Addr().load("ffee:1f2d:c587:24c3:9128:3349:3cee:143"));
+
+ IP4Addr lo{"127.0.0.1"};
+ CHECK(lo.is_loopback());
+ CHECK_FALSE(lo.is_any());
+ CHECK_FALSE(lo.is_multicast());
+ CHECK_FALSE(lo.is_link_local());
+ CHECK(lo[0] == 0x7F);
+
+ IP4Addr any{"0.0.0.0"};
+ REQUIRE_FALSE(any.is_loopback());
+ REQUIRE(any.is_any());
+ REQUIRE_FALSE(any.is_link_local());
+ REQUIRE(any == IP4Addr("0"));
+
+ IP4Addr mc{"238.11.55.99"};
+ CHECK_FALSE(mc.is_loopback());
+ CHECK_FALSE(mc.is_any());
+ CHECK_FALSE(mc.is_link_local());
+ CHECK(mc.is_multicast());
+
+ IP4Addr ll4{"169.254.55.99"};
+ CHECK_FALSE(ll4.is_loopback());
+ CHECK_FALSE(ll4.is_any());
+ CHECK(ll4.is_link_local());
+ CHECK_FALSE(ll4.is_multicast());
+ CHECK(swoc::ip::is_link_local_host_order(ll4.host_order()));
+ CHECK_FALSE(swoc::ip::is_link_local_network_order(ll4.host_order()));
+
+ CHECK(swoc::ip::is_private_host_order(0xC0A8BADC));
+ CHECK_FALSE(swoc::ip::is_private_network_order(0xC0A8BADC));
+ CHECK_FALSE(swoc::ip::is_private_host_order(0xDCBA8C0));
+ CHECK(swoc::ip::is_private_network_order(0xDCBA8C0));
+
+ CHECK(IP4Addr(INADDR_LOOPBACK).is_loopback());
+
+ IP6Addr lo6{"::1"};
+ REQUIRE(lo6.is_loopback());
+ REQUIRE_FALSE(lo6.is_any());
+ REQUIRE_FALSE(lo6.is_multicast());
+ REQUIRE_FALSE(lo.is_link_local());
+
+ IP6Addr any6{"::"};
+ REQUIRE_FALSE(any6.is_loopback());
+ REQUIRE(any6.is_any());
+ REQUIRE_FALSE(lo.is_link_local());
+
+ IP6Addr multi6{"FF02::19"};
+ REQUIRE(multi6.is_loopback() == false);
+ REQUIRE(multi6.is_multicast() == true);
+ REQUIRE(lo.is_link_local() == false);
+ REQUIRE(IPAddr(multi6).is_multicast());
+
+ IP6Addr ll{"FE80::56"};
+ REQUIRE(ll.is_link_local() == true);
+ REQUIRE(ll.is_multicast() == false);
+ REQUIRE(IPAddr(ll).is_link_local() == true);
+
+ // Do a bit of IPv6 testing.
+ IP6Addr a6_null;
+ IP6Addr a6_1{"fe80:88b5:4a:20c:29ff:feae:5587:1c33"};
+ IP6Addr a6_2{"fe80:88b5:4a:20c:29ff:feae:5587:1c34"};
+ IP6Addr a6_3{"de80:88b5:4a:20c:29ff:feae:5587:1c35"};
+
+ REQUIRE(a6_1 != a6_null);
+ REQUIRE(a6_1 != a6_2);
+ REQUIRE(a6_1 < a6_2);
+ REQUIRE(a6_2 > a6_1);
+ ++a6_1;
+ REQUIRE(a6_1 == a6_2);
+ ++a6_1;
+ REQUIRE(a6_1 != a6_2);
+ REQUIRE(a6_1 > a6_2);
+
+ REQUIRE(a6_3 != a6_2);
+ REQUIRE(a6_3 < a6_2);
+ REQUIRE(a6_2 > a6_3);
+
+ REQUIRE(-1 == a6_3.cmp(a6_2));
+ REQUIRE(0 == a6_2.cmp(a6_2));
+ REQUIRE(1 == a6_1.cmp(a6_2));
+
+ REQUIRE(a6_1[0] == 0xFE);
+ REQUIRE(a6_1[1] == 0x80);
+ REQUIRE(a6_2[3] == 0xB5);
+ REQUIRE(a6_3[11] == 0xAE);
+ REQUIRE(a6_3[14] == 0x1C);
+ REQUIRE(a6_2[15] == 0x34);
+
+ REQUIRE(a6_1.host_order() != a6_2.host_order());
+
+ a6_1.copy_to(&ep.sa);
+ REQUIRE(a6_1 == IP6Addr(ep.ip6()));
+ REQUIRE(IPAddr(a6_1) == &ep.sa);
+ REQUIRE(IPAddr(a6_2) != &ep.sa);
+ a6_2.copy_to(&ep.sa6);
+ REQUIRE(a6_2 == IP6Addr(&ep.sa6));
+ REQUIRE(a6_1 != IP6Addr(ep.ip6()));
+ in6_addr in6;
+ a6_1.network_order(in6);
+ REQUIRE(a6_1 == IP6Addr(in6));
+ a6_1.network_order(ep.sa6.sin6_addr);
+ REQUIRE(a6_1 == IP6Addr(ep.ip6()));
+ in6 = a6_2.network_order();
+ REQUIRE(a6_2.host_order() != in6);
+ REQUIRE(a6_2.network_order() == in6);
+ REQUIRE(a6_2 == IP6Addr(in6));
+ a6_2.host_order(in6);
+ REQUIRE(a6_2.network_order() != in6);
+ REQUIRE(a6_2.host_order() == in6);
+ REQUIRE(in6.s6_addr[0] == 0x34);
+ REQUIRE(in6.s6_addr[6] == 0xff);
+ REQUIRE(in6.s6_addr[13] == 0x88);
+
+ // Little bit of IP4 address arithmetic / comparison testing.
+ IP4Addr a4_null;
+ IP4Addr a4_1{"172.28.56.33"};
+ IP4Addr a4_2{"172.28.56.34"};
+ IP4Addr a4_3{"170.28.56.35"};
+ IP4Addr a4_loopback{"127.0.0.1"_tv};
+ IP4Addr ip4_loopback{INADDR_LOOPBACK};
+
+ REQUIRE(a4_loopback == ip4_loopback);
+ REQUIRE(a4_loopback.is_loopback() == true);
+ REQUIRE(ip4_loopback.is_loopback() == true);
+ CHECK(a4_2.is_private());
+ CHECK_FALSE(a4_3.is_private());
+
+ REQUIRE(a4_1 != a4_null);
+ REQUIRE(a4_1 != a4_2);
+ REQUIRE(a4_1 < a4_2);
+ REQUIRE(a4_2 > a4_1);
+ ++a4_1;
+ REQUIRE(a4_1 == a4_2);
+ ++a4_1;
+ REQUIRE(a4_1 != a4_2);
+ REQUIRE(a4_1 > a4_2);
+ REQUIRE(a4_3 != a4_2);
+ REQUIRE(a4_3 < a4_2);
+ REQUIRE(a4_2 > a4_3);
+
+ REQUIRE(IPAddr(a4_1) > IPAddr(a4_2));
+ REQUIRE(IPAddr(a4_1) >= IPAddr(a4_2));
+ REQUIRE(false == (IPAddr(a4_1) < IPAddr(a4_2)));
+ REQUIRE(IPAddr(a6_2) < IPAddr(a6_1));
+ REQUIRE(IPAddr(a6_2) <= IPAddr(a6_1));
+ REQUIRE(false == (IPAddr(a6_2) > IPAddr(a6_1)));
+ REQUIRE(IPAddr(a4_3) == IPAddr(a4_3));
+ REQUIRE(IPAddr(a4_3) <= IPAddr(a4_3));
+ REQUIRE(IPAddr(a4_3) >= IPAddr(a4_3));
+ REQUIRE(IPAddr(a4_3) < IPAddr(a6_3));
+ REQUIRE(IPAddr{} < IPAddr(a4_3));
+ REQUIRE(IPAddr{} == IPAddr{});
+
+ REQUIRE(IPAddr(a4_3).cmp(IPAddr(a6_3)) == -1);
+ REQUIRE(IPAddr{}.cmp(IPAddr(a4_3)) == -1);
+ REQUIRE(IPAddr{}.cmp(IPAddr{}) == 0);
+ REQUIRE(IPAddr(a6_3).cmp(IPAddr(a4_3)) == 1);
+ REQUIRE(IPAddr{a4_3}.cmp(IPAddr{}) == 1);
+
+ // For this data, the bytes should be in IPv6 network order.
+ static const std::tuple<TextView, bool, IP6Addr::raw_type> ipv6_ex[] = {
+ {"::", true, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
+ {"::1", true, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}},
+ {":::", false, {} },
+ {"fe80::20c:29ff:feae:5587:1c33",
+ true, {0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0C, 0x29, 0xFF, 0xFE, 0xAE, 0x55, 0x87, 0x1C, 0x33}},
+ {"fe80:20c:29ff:feae:5587::1c33",
+ true, {0xFE, 0x80, 0x02, 0x0C, 0x29, 0xFF, 0xFE, 0xAE, 0x55, 0x87, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x33}},
+ {"fe80:20c:29ff:feae:5587:1c33::",
+ true, {0xFE, 0x80, 0x02, 0x0C, 0x29, 0xFF, 0xFE, 0xAE, 0x55, 0x87, 0x1c, 0x33, 0x00, 0x00, 0x00, 0x00}},
+ {"::fe80:20c:29ff:feae:5587:1c33",
+ true, {0x00, 0x00, 0x00, 0x00, 0xFE, 0x80, 0x02, 0x0C, 0x29, 0xFF, 0xFE, 0xAE, 0x55, 0x87, 0x1c, 0x33}},
+ {":fe80:20c:29ff:feae:5587:4A43:1c33", false, {} },
+ {"fe80:20c::29ff:feae:5587::1c33", false, {} }
+ };
+
+ for (auto const &item : ipv6_ex) {
+ auto &&[text, result, data]{item};
+ IP6Addr addr;
+ REQUIRE(result == addr.load(text));
+ if (result) {
+ union {
+ in6_addr _inet;
+ IP6Addr::raw_type _raw;
+ } ar;
+ ar._inet = addr.network_order();
+ REQUIRE(ar._raw == data);
+ }
+ }
+
+ IPRange r;
+ IP4Range r4;
+ IP6Range r6;
+
+ REQUIRE(r4.load("10.242.129.0-10.242.129.127") == true);
+ REQUIRE(r4.min() == IP4Addr("10.242.129.0"));
+ REQUIRE(r4.max() == IP4Addr("10.242.129.127"));
+ REQUIRE(r4.load("10.242.129.0/25") == true);
+ REQUIRE(r4.min() == IP4Addr("10.242.129.0"));
+ REQUIRE(r4.max() == IP4Addr("10.242.129.127"));
+ REQUIRE(r4.load("2.2.2.2") == true);
+ REQUIRE(r4.min() == IP4Addr("2.2.2.2"));
+ REQUIRE(r4.max() == IP4Addr("2.2.2.2"));
+ REQUIRE(r4.load("2.2.2.2.2") == false);
+ REQUIRE(r4.load("2.2.2.2-fe80:20c::29ff:feae:5587::1c33") == false);
+ CHECK(r4.load("0xC0A83801"));
+ REQUIRE(r4 == IP4Addr("192.168.56.1"));
+
+ // A few special cases.
+ static constexpr TextView all_4_txt{"0/0"};
+ static constexpr TextView all_6_txt{"::/0"};
+
+ CHECK(r4.load(all_4_txt));
+ CHECK(r.load(all_4_txt));
+ REQUIRE(r.ip4() == r4);
+ REQUIRE(r4.min() == IP4Addr::MIN);
+ REQUIRE(r4.max() == IP4Addr::MAX);
+ CHECK(r.load(all_6_txt));
+ CHECK(r6.load(all_6_txt));
+ REQUIRE(r.ip6() == r6);
+ REQUIRE(r6.min() == IP6Addr::MIN);
+ REQUIRE(r6.max() == IP6Addr::MAX);
+ CHECK_FALSE(r6.load("2.2.2.2-fe80:20c::29ff:feae:5587::1c33"));
+ CHECK_FALSE(r.load("2.2.2.2-fe80:20c::29ff:feae:5587::1c33"));
+
+ ep.set_to_any(AF_INET);
+ REQUIRE(ep.is_loopback() == false);
+ REQUIRE(ep.is_any() == true);
+ REQUIRE(ep.raw_addr().length() == sizeof(in_addr_t));
+ ep.set_to_loopback(AF_INET6);
+ REQUIRE(ep.is_loopback() == true);
+ REQUIRE(ep.is_any() == false);
+ REQUIRE(ep.raw_addr().length() == sizeof(in6_addr));
+
+ ep.set_to_any(AF_INET6);
+ REQUIRE(ep.is_loopback() == false);
+ REQUIRE(ep.is_any() == true);
+ CHECK(ep.ip4() == nullptr);
+ IP6Addr a6{ep.ip6()};
+ REQUIRE(a6.is_loopback() == false);
+ REQUIRE(a6.is_any() == true);
+
+ ep.set_to_loopback(AF_INET);
+ REQUIRE(ep.is_loopback() == true);
+ REQUIRE(ep.is_any() == false);
+ CHECK(ep.ip6() == nullptr);
+ IP4Addr a4{ep.ip4()};
+ REQUIRE(a4.is_loopback() == true);
+ REQUIRE(a4.is_any() == false);
+
+ CHECK_FALSE(IP6Addr("1337:0:0:ded:BEEF:0:0:0").is_mapped_ip4());
+ CHECK_FALSE(IP6Addr("1337:0:0:ded:BEEF::").is_mapped_ip4());
+ CHECK(IP6Addr("::FFFF:C0A8:381F").is_mapped_ip4());
+ CHECK_FALSE(IP6Addr("FFFF:C0A8:381F::").is_mapped_ip4());
+ CHECK_FALSE(IP6Addr("::C0A8:381F").is_mapped_ip4());
+ CHECK(IP6Addr(a4_2).is_mapped_ip4());
+};
+
+TEST_CASE("IP Net and Mask", "[libswoc][ip][ipnet]") {
+ IP4Addr a24{"255.255.255.0"};
+ REQUIRE(IP4Addr::MAX == IPMask(32).as_ip4());
+ REQUIRE(IP4Addr::MIN == IPMask(0).as_ip4());
+ REQUIRE(IPMask(24).as_ip4() == a24);
+
+ SECTION("addr as mask") {
+ swoc::IP4Net n1{"10.0.0.0/255.255.0.0"};
+ CHECK_FALSE(n1.empty());
+ REQUIRE(n1.mask().width() == 16);
+
+ swoc::IP6Net n2{"BEEF:1337:dead::/FFFF:FFFF:FFFF:C000::"};
+ CHECK_FALSE(n2.empty());
+ REQUIRE(n2.mask().width() == 50);
+
+ swoc::IPNet n3{"10.0.0.0/255.255.0.0"};
+ CHECK_FALSE(n3.empty());
+ REQUIRE(n3.mask().width() == 16);
+
+ swoc::IPNet n4{"BEEF:1337:dead::/FFFF:FFFF:FFFF:C000::"};
+ CHECK_FALSE(n4.empty());
+ REQUIRE(n4.mask().width() == 50);
+
+ swoc::IPNet n5{"BEEF:1337:dead::/FFFF:FFFF:FFFF:000C::"};
+ REQUIRE(n5.empty()); // mask address isn't a valid mask.
+ }
+
+ swoc::IP4Net n1{"0/1"};
+ auto nr1 = n1.as_range();
+ REQUIRE(nr1.min() == IP4Addr::MIN);
+ REQUIRE(nr1.max() == IP4Addr("127.255.255.255"));
+
+ IP4Addr a{"8.8.8.8"};
+ swoc::IP4Net n4{a, IPMask{32}};
+ auto nr4 = n4.as_range();
+ REQUIRE(nr4.min() == a);
+ REQUIRE(nr4.max() == a);
+
+ swoc::IP4Net n0{"0/0"};
+ auto nr0 = n0.as_range();
+ REQUIRE(nr0.min() == IP4Addr::MIN);
+ REQUIRE(nr0.max() == IP4Addr::MAX);
+
+ swoc::IPMask m128{128};
+ REQUIRE(m128.as_ip6() == IP6Addr::MAX);
+ swoc::IPMask m0{0};
+ REQUIRE(m0.as_ip6() == IP6Addr::MIN);
+
+ IP6Addr a6{"12:34:56:78:9A:BC:DE:FF"};
+ REQUIRE(a6 == (a6 | IPMask(128))); // Host network, should be unchanged.
+ REQUIRE(IP6Addr::MAX == (a6 | IPMask(0)));
+ REQUIRE(IP6Addr::MIN == (a6 & IPMask(0)));
+
+ IP6Addr a6_2{"2001:1f2d:c587:24c3:9128:3349:3cee:143"_tv};
+ swoc::IPMask mask{127};
+ CHECK(a6_2 == (a6_2 | mask));
+ CHECK(a6_2 != (a6_2 & mask));
+ CHECK(a6_2 == (a6_2 & swoc::IPMask(128))); // should always be a no-op.
+
+ IP6Net n6_1{a6_2, IPMask(96)};
+ CHECK(n6_1.min() == IP6Addr("2001:1f2d:c587:24c3:9128:3349::"));
+
+ swoc::IP6Addr a6_3{"2001:1f2d:c587:24c4::"};
+ CHECK(a6_3 == (a6_3 & swoc::IPMask{64}));
+ CHECK(a6_3 == (a6_3 & swoc::IPMask{62}));
+ CHECK(a6_3 != (a6_3 & swoc::IPMask{61}));
+
+ REQUIRE(IPMask(1) == IPMask::mask_for(IP4Addr("0x80.0.0.0")));
+ REQUIRE(IPMask(2) == IPMask::mask_for(IP4Addr("0xC0.0.0.0")));
+ REQUIRE(IPMask(27) == IPMask::mask_for(IP4Addr("0xFF.0xFF.0xFF.0xE0")));
+ REQUIRE(IPMask(55) == IPMask::mask_for(IP6Addr("1337:dead:beef:CA00::")));
+ REQUIRE(IPMask(91) == IPMask::mask_for(IP6Addr("1337:dead:beef:CA00:24c3:3ce0::")));
+
+ IP4Addr b1{"192.168.56.24"};
+ REQUIRE((b1 & IPMask(24)) == IP4Addr("192.168.56.0"));
+ IP6Addr b2{"1337:dead:beef:CA00:24c3:3ce0:9120:143"};
+ REQUIRE((b2 & IPMask(32)) == IP6Addr("1337:dead::"));
+ REQUIRE((b2 & IPMask(64)) == IP6Addr("1337:dead:beef:CA00::"));
+ REQUIRE((b2 & IPMask(96)) == IP6Addr("1337:dead:beef:CA00:24c3:3ce0::"));
+ // do it again with generic address.
+ IPAddr b3{"192.168.56.24"};
+ REQUIRE((b3 & IPMask(24)) == IP4Addr("192.168.56.0"));
+ IPAddr b4{"1337:dead:beef:CA00:24c3:3ce0:9120:143"};
+ REQUIRE((b4 & IPMask(32)) == IP6Addr("1337:dead::"));
+ REQUIRE((b4 & IPMask(64)) == IP6Addr("1337:dead:beef:CA00::"));
+ REQUIRE((b4 & IPMask(96)) == IP6Addr("1337:dead:beef:CA00:24c3:3ce0::"));
+
+ IP4Addr c1{"192.168.56.24"};
+ REQUIRE((c1 | IPMask(24)) == IP4Addr("192.168.56.255"));
+ REQUIRE((c1 | IPMask(15)) == IP4Addr("192.169.255.255"));
+ REQUIRE((c1 | IPMask(7)) == IP4Addr("193.255.255.255"));
+ IP6Addr c2{"1337:dead:beef:CA00:24c3:3ce0:9120:143"};
+ REQUIRE((c2 | IPMask(96)) == IP6Addr("1337:dead:beef:CA00:24c3:3ce0:FFFF:FFFF"));
+ REQUIRE((c2 | IPMask(64)) == IP6Addr("1337:dead:beef:CA00:FFFF:FFFF:FFFF:FFFF"));
+ REQUIRE((c2 | IPMask(32)) == IP6Addr("1337:dead:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"));
+ // do it again with generic address.
+ IPAddr c3{"192.168.56.24"};
+ REQUIRE((c3 | IPMask(24)) == IP4Addr("192.168.56.255"));
+ REQUIRE((c3 | IPMask(15)) == IP4Addr("192.169.255.255"));
+ REQUIRE((c3 | IPMask(7)) == IP4Addr("193.255.255.255"));
+ IPAddr c4{"1337:dead:beef:CA00:24c3:3ce0:9120:143"};
+ REQUIRE((c4 | IPMask(96)) == IP6Addr("1337:dead:beef:CA00:24c3:3ce0:FFFF:FFFF"));
+ REQUIRE((c4 | IPMask(64)) == IP6Addr("1337:dead:beef:CA00:FFFF:FFFF:FFFF:FFFF"));
+ REQUIRE((c4 | IPMask(32)) == IP6Addr("1337:dead:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"));
+}
+
+TEST_CASE("IP Formatting", "[libswoc][ip][bwformat]") {
+ IPEndpoint ep;
+ std::string_view addr_1{"[ffee::24c3:3349:3cee:143]:8080"};
+ std::string_view addr_2{"172.17.99.231:23995"};
+ std::string_view addr_3{"[1337:ded:BEEF::]:53874"};
+ std::string_view addr_4{"[1337::ded:BEEF]:53874"};
+ std::string_view addr_5{"[1337:0:0:ded:BEEF:0:0:956]:53874"};
+ std::string_view addr_6{"[1337:0:0:ded:BEEF:0:0:0]:53874"};
+ std::string_view addr_7{"172.19.3.105:4951"};
+ std::string_view addr_8{"[1337:0:0:ded:BEEF:0:0:0]"};
+ std::string_view addr_9{"1337:0:0:ded:BEEF:0:0:0"};
+ std::string_view addr_A{"172.19.3.105"};
+ std::string_view addr_null{"[::]:53874"};
+ std::string_view localhost{"[::1]:8080"};
+ swoc::LocalBufferWriter<1024> w;
+
+ REQUIRE(ep.parse(addr_null) == true);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "::");
+
+ ep.set_to_loopback(AF_INET6);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "::1");
+
+ REQUIRE(ep.parse(addr_1) == true);
+ w.clear().print("{}", ep);
+ REQUIRE(w.view() == addr_1);
+ w.clear().print("{::p}", ep);
+ REQUIRE(w.view() == "8080");
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == addr_1.substr(1, 24)); // check the brackets are dropped.
+ w.clear().print("[{::a}]", ep);
+ REQUIRE(w.view() == addr_1.substr(0, 26)); // check the brackets are dropped.
+ w.clear().print("[{0::a}]:{0::p}", ep);
+ REQUIRE(w.view() == addr_1); // check the brackets are dropped.
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "ffee:0000:0000:0000:24c3:3349:3cee:0143");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "ffee: 0: 0: 0:24c3:3349:3cee: 143");
+
+ // Verify @c IPEndpoint will parse without the port.
+ REQUIRE(ep.parse(addr_8) == true);
+ REQUIRE(ep.network_order_port() == 0);
+ REQUIRE(ep.parse(addr_9) == true);
+ REQUIRE(ep.network_order_port() == 0);
+ REQUIRE(ep.parse(addr_A) == true);
+ REQUIRE(ep.network_order_port() == 0);
+
+ REQUIRE(ep.parse(addr_2) == true);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == addr_2.substr(0, 13));
+ w.clear().print("{0::a}", ep);
+ REQUIRE(w.view() == addr_2.substr(0, 13));
+ w.clear().print("{::ap}", ep);
+ REQUIRE(w.view() == addr_2);
+ w.clear().print("{::f}", ep);
+ REQUIRE(w.view() == "ipv4");
+ w.clear().print("{::fpa}", ep);
+ REQUIRE(w.view() == "172.17.99.231:23995 ipv4");
+ w.clear().print("{0::a} .. {0::p}", ep);
+ REQUIRE(w.view() == "172.17.99.231 .. 23995");
+ w.clear().print("<+> {0::a} <+> {0::p}", ep);
+ REQUIRE(w.view() == "<+> 172.17.99.231 <+> 23995");
+ w.clear().print("<+> {0::a} <+> {0::p} <+>", ep);
+ REQUIRE(w.view() == "<+> 172.17.99.231 <+> 23995 <+>");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "172. 17. 99.231");
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "172.017.099.231");
+ w.clear().print("{:x:a}", ep);
+ REQUIRE(w.view() == "ac.11.63.e7");
+ auto a4 = IP4Addr(ep.ip4());
+ w.clear().print("{:x}", a4);
+ REQUIRE(w.view() == "ac.11.63.e7");
+
+ REQUIRE(ep.parse(addr_3) == true);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337:ded:beef::"_tv);
+
+ REQUIRE(ep.parse(addr_4) == true);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337::ded:beef"_tv);
+
+ REQUIRE(ep.parse(addr_5) == true);
+ w.clear().print("{:X:a}", ep);
+ REQUIRE(w.view() == "1337::DED:BEEF:0:0:956");
+
+ REQUIRE(ep.parse(addr_6) == true);
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "1337:0:0:ded:beef::");
+
+ // Documentation examples
+ REQUIRE(ep.parse(addr_7) == true);
+ w.clear().print("To {}", ep);
+ REQUIRE(w.view() == "To 172.19.3.105:4951");
+ w.clear().print("To {0::a} on port {0::p}", ep); // no need to pass the argument twice.
+ REQUIRE(w.view() == "To 172.19.3.105 on port 4951");
+ w.clear().print("To {::=}", ep);
+ REQUIRE(w.view() == "To 172.019.003.105:04951");
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == "172.19.3.105");
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "172.019.003.105");
+ w.clear().print("{::0=a}", ep);
+ REQUIRE(w.view() == "172.019.003.105");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == "172. 19. 3.105");
+ w.clear().print("{:>20:a}", ep);
+ REQUIRE(w.view() == " 172.19.3.105");
+ w.clear().print("{:>20:=a}", ep);
+ REQUIRE(w.view() == " 172.019.003.105");
+ w.clear().print("{:>20: =a}", ep);
+ REQUIRE(w.view() == " 172. 19. 3.105");
+ w.clear().print("{:<20:a}", ep);
+ REQUIRE(w.view() == "172.19.3.105 ");
+
+ REQUIRE(ep.parse(localhost) == true);
+ w.clear().print("{}", ep);
+ REQUIRE(w.view() == localhost);
+ w.clear().print("{::p}", ep);
+ REQUIRE(w.view() == "8080");
+ w.clear().print("{::a}", ep);
+ REQUIRE(w.view() == localhost.substr(1, 3)); // check the brackets are dropped.
+ w.clear().print("[{::a}]", ep);
+ REQUIRE(w.view() == localhost.substr(0, 5));
+ w.clear().print("[{0::a}]:{0::p}", ep);
+ REQUIRE(w.view() == localhost); // check the brackets are dropped.
+ w.clear().print("{::=a}", ep);
+ REQUIRE(w.view() == "0000:0000:0000:0000:0000:0000:0000:0001");
+ w.clear().print("{:: =a}", ep);
+ REQUIRE(w.view() == " 0: 0: 0: 0: 0: 0: 0: 1");
+
+ std::string_view r_1{"10.1.0.0-10.1.0.127"};
+ std::string_view r_2{"10.2.0.1-10.2.0.127"}; // not a network - bad start
+ std::string_view r_3{"10.3.0.0-10.3.0.126"}; // not a network - bad end
+ std::string_view r_4{"10.4.1.1-10.4.1.1"}; // singleton
+ std::string_view r_5{"10.20.30.40- 50.60.70.80"};
+ std::string_view r_6{"10.20.30.40 -50.60.70.80"};
+ std::string_view r_7{"10.20.30.40 - 50.60.70.80"};
+
+ IPRange r;
+
+ r.load(r_1);
+ w.clear().print("{}", r);
+ REQUIRE(w.view() == r_1);
+ w.clear().print("{::c}", r);
+ REQUIRE(w.view() == "10.1.0.0/25");
+
+ r.load(r_2);
+ w.clear().print("{}", r);
+ REQUIRE(w.view() == r_2);
+ w.clear().print("{::c}", r);
+ REQUIRE(w.view() == r_2);
+
+ r.load(r_3);
+ w.clear().print("{}", r);
+ REQUIRE(w.view() == r_3);
+ w.clear().print("{::c}", r);
+ REQUIRE(w.view() == r_3);
+
+ r.load(r_4);
+ w.clear().print("{}", r);
+ REQUIRE(w.view() == r_4);
+ w.clear().print("{::c}", r);
+ REQUIRE(w.view() == "10.4.1.1");
+
+ REQUIRE(r.load(r_5));
+ REQUIRE(r.load(r_6));
+ REQUIRE(r.load(r_7));
+}
+
+TEST_CASE("IP ranges and networks", "[libswoc][ip][net][range]") {
+ swoc::IP4Range r_0;
+ swoc::IP4Range r_1{"1.1.1.0-1.1.1.9"};
+ swoc::IP4Range r_2{"1.1.2.0-1.1.2.97"};
+ swoc::IP4Range r_3{"1.1.0.0-1.2.0.0"};
+ swoc::IP4Range r_4{"10.33.45.19-10.33.45.76"};
+ swoc::IP6Range r_5{"2001:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"_tv};
+
+ CHECK(r_0.empty());
+ CHECK_FALSE(r_1.empty());
+
+ // Verify a family specific range only works with the same family range.
+ TextView r4_txt{"10.33.45.19-10.33.45.76"};
+ TextView r6_txt{"2001:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"};
+ IP4Range rr4;
+ IP6Range rr6;
+ CHECK(rr4.load(r4_txt));
+ CHECK_FALSE(rr4.load(r6_txt));
+ CHECK_FALSE(rr6.load(r4_txt));
+ CHECK(rr6.load(r6_txt));
+
+ std::array<swoc::IP4Net, 7> r_4_nets = {
+ {"10.33.45.19/32"_tv, "10.33.45.20/30"_tv, "10.33.45.24/29"_tv, "10.33.45.32/27"_tv, "10.33.45.64/29"_tv, "10.33.45.72/30"_tv,
+ "10.33.45.76/32"_tv}
+ };
+ auto r4_net = r_4_nets.begin();
+ for (auto net : r_4.networks()) {
+ REQUIRE(r4_net != r_4_nets.end());
+ CHECK(*r4_net == net);
+ ++r4_net;
+ }
+
+ // Let's try that again, with @c IPRange instead.
+ r4_net = r_4_nets.begin();
+ for (auto const &net : IPRange{r_4}.networks()) {
+ REQUIRE(r4_net != r_4_nets.end());
+ CHECK(*r4_net == net);
+ ++r4_net;
+ }
+
+ std::array<swoc::IP6Net, 130> r_5_nets = {
+ {{IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:143"}, IPMask{128}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:144"}, IPMask{126}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:148"}, IPMask{125}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:150"}, IPMask{124}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:160"}, IPMask{123}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:180"}, IPMask{121}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:200"}, IPMask{119}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:400"}, IPMask{118}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:800"}, IPMask{117}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:1000"}, IPMask{116}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:2000"}, IPMask{115}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:4000"}, IPMask{114}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cee:8000"}, IPMask{113}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cef:0"}, IPMask{112}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3cf0:0"}, IPMask{108}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3d00:0"}, IPMask{104}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:3e00:0"}, IPMask{103}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:4000:0"}, IPMask{98}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3349:8000:0"}, IPMask{97}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:334a::"}, IPMask{95}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:334c::"}, IPMask{94}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3350::"}, IPMask{92}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3360::"}, IPMask{91}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3380::"}, IPMask{89}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3400::"}, IPMask{86}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:3800::"}, IPMask{85}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:4000::"}, IPMask{82}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9128:8000::"}, IPMask{81}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9129::"}, IPMask{80}},
+ {IP6Addr{"2001:1f2d:c587:24c3:912a::"}, IPMask{79}},
+ {IP6Addr{"2001:1f2d:c587:24c3:912c::"}, IPMask{78}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9130::"}, IPMask{76}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9140::"}, IPMask{74}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9180::"}, IPMask{73}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9200::"}, IPMask{71}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9400::"}, IPMask{70}},
+ {IP6Addr{"2001:1f2d:c587:24c3:9800::"}, IPMask{69}},
+ {IP6Addr{"2001:1f2d:c587:24c3:a000::"}, IPMask{67}},
+ {IP6Addr{"2001:1f2d:c587:24c3:c000::"}, IPMask{66}},
+ {IP6Addr{"2001:1f2d:c587:24c4::"}, IPMask{62}},
+ {IP6Addr{"2001:1f2d:c587:24c8::"}, IPMask{61}},
+ {IP6Addr{"2001:1f2d:c587:24d0::"}, IPMask{60}},
+ {IP6Addr{"2001:1f2d:c587:24e0::"}, IPMask{59}},
+ {IP6Addr{"2001:1f2d:c587:2500::"}, IPMask{56}},
+ {IP6Addr{"2001:1f2d:c587:2600::"}, IPMask{55}},
+ {IP6Addr{"2001:1f2d:c587:2800::"}, IPMask{53}},
+ {IP6Addr{"2001:1f2d:c587:3000::"}, IPMask{52}},
+ {IP6Addr{"2001:1f2d:c587:4000::"}, IPMask{50}},
+ {IP6Addr{"2001:1f2d:c587:8000::"}, IPMask{49}},
+ {IP6Addr{"2001:1f2d:c588::"}, IPMask{45}},
+ {IP6Addr{"2001:1f2d:c590::"}, IPMask{44}},
+ {IP6Addr{"2001:1f2d:c5a0::"}, IPMask{43}},
+ {IP6Addr{"2001:1f2d:c5c0::"}, IPMask{42}},
+ {IP6Addr{"2001:1f2d:c600::"}, IPMask{39}},
+ {IP6Addr{"2001:1f2d:c800::"}, IPMask{37}},
+ {IP6Addr{"2001:1f2d:d000::"}, IPMask{36}},
+ {IP6Addr{"2001:1f2d:e000::"}, IPMask{35}},
+ {IP6Addr{"2001:1f2e::"}, IPMask{31}},
+ {IP6Addr{"2001:1f30::"}, IPMask{28}},
+ {IP6Addr{"2001:1f40::"}, IPMask{26}},
+ {IP6Addr{"2001:1f80::"}, IPMask{25}},
+ {IP6Addr{"2001:2000::"}, IPMask{19}},
+ {IP6Addr{"2001:4000::"}, IPMask{18}},
+ {IP6Addr{"2001:8000::"}, IPMask{17}},
+ {IP6Addr{"2002::"}, IPMask{15}},
+ {IP6Addr{"2004::"}, IPMask{14}},
+ {IP6Addr{"2008::"}, IPMask{13}},
+ {IP6Addr{"2010::"}, IPMask{12}},
+ {IP6Addr{"2020::"}, IPMask{11}},
+ {IP6Addr{"2040::"}, IPMask{10}},
+ {IP6Addr{"2080::"}, IPMask{9}},
+ {IP6Addr{"2100::"}, IPMask{8}},
+ {IP6Addr{"2200::"}, IPMask{7}},
+ {IP6Addr{"2400::"}, IPMask{6}},
+ {IP6Addr{"2800::"}, IPMask{5}},
+ {IP6Addr{"3000::"}, IPMask{4}},
+ {IP6Addr{"4000::"}, IPMask{2}},
+ {IP6Addr{"8000::"}, IPMask{2}},
+ {IP6Addr{"c000::"}, IPMask{3}},
+ {IP6Addr{"e000::"}, IPMask{4}},
+ {IP6Addr{"f000::"}, IPMask{5}},
+ {IP6Addr{"f800::"}, IPMask{6}},
+ {IP6Addr{"fc00::"}, IPMask{7}},
+ {IP6Addr{"fe00::"}, IPMask{8}},
+ {IP6Addr{"ff00::"}, IPMask{9}},
+ {IP6Addr{"ff80::"}, IPMask{10}},
+ {IP6Addr{"ffc0::"}, IPMask{11}},
+ {IP6Addr{"ffe0::"}, IPMask{13}},
+ {IP6Addr{"ffe8::"}, IPMask{14}},
+ {IP6Addr{"ffec::"}, IPMask{15}},
+ {IP6Addr{"ffee::"}, IPMask{20}},
+ {IP6Addr{"ffee:1000::"}, IPMask{21}},
+ {IP6Addr{"ffee:1800::"}, IPMask{22}},
+ {IP6Addr{"ffee:1c00::"}, IPMask{23}},
+ {IP6Addr{"ffee:1e00::"}, IPMask{24}},
+ {IP6Addr{"ffee:1f00::"}, IPMask{27}},
+ {IP6Addr{"ffee:1f20::"}, IPMask{29}},
+ {IP6Addr{"ffee:1f28::"}, IPMask{30}},
+ {IP6Addr{"ffee:1f2c::"}, IPMask{32}},
+ {IP6Addr{"ffee:1f2d::"}, IPMask{33}},
+ {IP6Addr{"ffee:1f2d:8000::"}, IPMask{34}},
+ {IP6Addr{"ffee:1f2d:c000::"}, IPMask{38}},
+ {IP6Addr{"ffee:1f2d:c400::"}, IPMask{40}},
+ {IP6Addr{"ffee:1f2d:c500::"}, IPMask{41}},
+ {IP6Addr{"ffee:1f2d:c580::"}, IPMask{46}},
+ {IP6Addr{"ffee:1f2d:c584::"}, IPMask{47}},
+ {IP6Addr{"ffee:1f2d:c586::"}, IPMask{48}},
+ {IP6Addr{"ffee:1f2d:c587::"}, IPMask{51}},
+ {IP6Addr{"ffee:1f2d:c587:2000::"}, IPMask{54}},
+ {IP6Addr{"ffee:1f2d:c587:2400::"}, IPMask{57}},
+ {IP6Addr{"ffee:1f2d:c587:2480::"}, IPMask{58}},
+ {IP6Addr{"ffee:1f2d:c587:24c0::"}, IPMask{63}},
+ {IP6Addr{"ffee:1f2d:c587:24c2::"}, IPMask{64}},
+ {IP6Addr{"ffee:1f2d:c587:24c3::"}, IPMask{65}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:8000::"}, IPMask{68}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9000::"}, IPMask{72}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9100::"}, IPMask{75}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9120::"}, IPMask{77}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128::"}, IPMask{83}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:2000::"}, IPMask{84}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3000::"}, IPMask{87}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3200::"}, IPMask{88}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3300::"}, IPMask{90}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3340::"}, IPMask{93}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3348::"}, IPMask{96}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3349::"}, IPMask{99}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3349:2000:0"}, IPMask{100}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3349:3000:0"}, IPMask{101}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3349:3800:0"}, IPMask{102}},
+ {IP6Addr{"ffee:1f2d:c587:24c3:9128:3349:3c00:0"}, IPMask{104}}}
+ };
+
+ auto r5_net = r_5_nets.begin();
+ for (auto const &[a, m] : r_5.networks()) {
+ REQUIRE(r5_net != r_5_nets.end());
+ CHECK(*r5_net == swoc::IP6Net{a, m});
+ ++r5_net;
+ }
+
+ // Try it again, using @c IPNet.
+ r5_net = r_5_nets.begin();
+ for (auto const &[a, m] : IPRange{r_5}.networks()) {
+ REQUIRE(r5_net != r_5_nets.end());
+ CHECK(*r5_net == swoc::IPNet{a, m});
+ ++r5_net;
+ }
+}
+
+TEST_CASE("IP Space Int", "[libswoc][ip][ipspace]") {
+ using uint_space = swoc::IPSpace<unsigned>;
+ uint_space space;
+
+ REQUIRE(space.count() == 0);
+
+ space.mark(
+ IPRange{
+ {IP4Addr("172.16.0.0"), IP4Addr("172.16.0.255")}
+ },
+ 1);
+ auto result = space.find(IPAddr{"172.16.0.97"});
+ REQUIRE(result != space.end());
+ REQUIRE(std::get<1>(*result) == 1);
+
+ result = space.find(IPAddr{"172.17.0.97"});
+ REQUIRE(result == space.end());
+
+ space.mark(IPRange{"172.16.0.12-172.16.0.25"_tv}, 2);
+
+ result = space.find(IPAddr{"172.16.0.21"});
+ REQUIRE(result != space.end());
+ REQUIRE(std::get<1>(*result) == 2);
+ REQUIRE(space.count() == 3);
+
+ space.clear();
+ auto BF = [](unsigned &lhs, unsigned rhs) -> bool {
+ lhs |= rhs;
+ return true;
+ };
+
+ swoc::IP4Range r_1{"1.1.1.0-1.1.1.9"};
+ swoc::IP4Range r_2{"1.1.2.0-1.1.2.97"};
+ swoc::IP4Range r_3{"1.1.0.0-1.2.0.0"};
+
+ // Compiler check - make sure both of these work.
+ REQUIRE(r_1.min() == IP4Addr("1.1.1.0"_tv));
+ REQUIRE(r_1.max() == IPAddr("1.1.1.9"_tv));
+
+ space.blend(r_1, 0x1, BF);
+ REQUIRE(space.count() == 1);
+ REQUIRE(space.end() == space.find(r_2.min()));
+ REQUIRE(space.end() != space.find(r_1.min()));
+ REQUIRE(space.end() != space.find(r_1.max()));
+ REQUIRE(space.end() != space.find(IP4Addr{"1.1.1.7"}));
+ CHECK(0x1 == std::get<1>(*space.find(IP4Addr{"1.1.1.7"})));
+
+ space.blend(r_2, 0x2, BF);
+ REQUIRE(space.count() == 2);
+ REQUIRE(space.end() != space.find(r_1.min()));
+ auto spot = space.find(r_2.min());
+ REQUIRE(spot != space.end());
+ REQUIRE(std::get<1>(*spot) == 0x2);
+ spot = space.find(r_2.max());
+ REQUIRE(spot != space.end());
+ REQUIRE(std::get<1>(*spot) == 0x2);
+
+ space.blend(r_3, 0x4, BF);
+ REQUIRE(space.count() == 5);
+ spot = space.find(r_2.min());
+ REQUIRE(spot != space.end());
+ REQUIRE(std::get<1>(*spot) == 0x6);
+
+ spot = space.find(r_3.min());
+ REQUIRE(spot != space.end());
+ REQUIRE(std::get<1>(*spot) == 0x4);
+
+ spot = space.find(r_1.max());
+ REQUIRE(spot != space.end());
+ REQUIRE(std::get<1>(*spot) == 0x5);
+
+ space.blend(IPRange{r_2.min(), r_3.max()}, 0x6, BF);
+ REQUIRE(space.count() == 4);
+
+ std::array<std::tuple<TextView, int>, 9> ranges = {
+ {{"100.0.0.0-100.0.0.255", 0},
+ {"100.0.1.0-100.0.1.255", 1},
+ {"100.0.2.0-100.0.2.255", 2},
+ {"100.0.3.0-100.0.3.255", 3},
+ {"100.0.4.0-100.0.4.255", 4},
+ {"100.0.5.0-100.0.5.255", 5},
+ {"100.0.6.0-100.0.6.255", 6},
+ {"100.0.0.0-100.0.0.255", 31},
+ {"100.0.1.0-100.0.1.255", 30}}
+ };
+
+ space.clear();
+ for (auto &&[text, value] : ranges) {
+ IPRange range{text};
+ space.mark(IPRange{text}, value);
+ }
+
+ CHECK(7 == space.count());
+ // Make sure all of these addresses yield the same result.
+ CHECK(space.end() != space.find(IP4Addr{"100.0.4.16"}));
+ CHECK(space.end() != space.find(IPAddr{"100.0.4.16"}));
+ CHECK(space.end() != space.find(IPAddr{IPEndpoint{"100.0.4.16:80"}}));
+ // same for negative result
+ CHECK(space.end() == space.find(IP4Addr{"10.0.4.16"}));
+ CHECK(space.end() == space.find(IPAddr{"10.0.4.16"}));
+ CHECK(space.end() == space.find(IPAddr{IPEndpoint{"10.0.4.16:80"}}));
+
+ std::array<std::tuple<TextView, int>, 3> r_clear = {
+ {{"2.2.2.2-2.2.2.40", 0}, {"2.2.2.50-2.2.2.60", 1}, {"2.2.2.70-2.2.2.100", 2}}
+ };
+ space.clear();
+ for (auto &&[text, value] : r_clear) {
+ IPRange range{text};
+ space.mark(IPRange{text}, value);
+ }
+ CHECK(space.count() == 3);
+ space.erase(IPRange{"2.2.2.35-2.2.2.75"});
+ CHECK(space.count() == 2);
+ {
+ spot = space.begin();
+ auto [r0, p0] = *spot;
+ auto [r2, p2] = *++spot;
+ CHECK(r0 == IPRange{"2.2.2.2-2.2.2.34"});
+ CHECK(p0 == 0);
+ CHECK(r2 == IPRange{"2.2.2.76-2.2.2.100"});
+ CHECK(p2 == 2);
+ }
+
+ // This is about testing repeated colorings of the same addresses, which happens quite a
+ // bit in normal network datasets. In fact, the test dataset is based on such a dataset
+ // and its use.
+ auto b2 = [](unsigned &lhs, unsigned const &rhs) {
+ lhs = rhs;
+ return true;
+ };
+ std::array<std::tuple<TextView, unsigned>, 31> r2 = {
+ {
+ {"2001:4998:58:400::1/128", 1} // 1
+ ,
+ {"2001:4998:58:400::2/128", 1},
+ {"2001:4998:58:400::3/128", 1},
+ {"2001:4998:58:400::4/128", 1},
+ {"2001:4998:58:400::5/128", 1},
+ {"2001:4998:58:400::6/128", 1},
+ {"2001:4998:58:400::7/128", 1},
+ {"2001:4998:58:400::8/128", 1},
+ {"2001:4998:58:400::9/128", 1},
+ {"2001:4998:58:400::A/127", 1},
+ {"2001:4998:58:400::10/127", 1} // 2
+ ,
+ {"2001:4998:58:400::12/127", 1},
+ {"2001:4998:58:400::14/127", 1},
+ {"2001:4998:58:400::16/127", 1},
+ {"2001:4998:58:400::18/127", 1},
+ {"2001:4998:58:400::1a/127", 1},
+ {"2001:4998:58:400::1c/127", 1},
+ {"2001:4998:58:400::1e/127", 1},
+ {"2001:4998:58:400::20/127", 1},
+ {"2001:4998:58:400::22/127", 1},
+ {"2001:4998:58:400::24/127", 1},
+ {"2001:4998:58:400::26/127", 1},
+ {"2001:4998:58:400::2a/127", 1} // 3
+ ,
+ {"2001:4998:58:400::2c/127", 1},
+ {"2001:4998:58:400::2e/127", 1},
+ {"2001:4998:58:400::30/127", 1},
+ {"2001:4998:58:400::140/127", 1} // 4
+ ,
+ {"2001:4998:58:400::142/127", 1},
+ {"2001:4998:58:400::146/127", 1} // 5
+ ,
+ {"2001:4998:58:400::148/127", 1},
+ {"2001:4998:58:400::150/127", 1} // 6
+ }
+ };
+
+ space.clear();
+ // Start with basic blending.
+ for (auto &&[text, value] : r2) {
+ IPRange range{text};
+ space.blend(IPRange{text}, value, b2);
+ REQUIRE(space.end() != space.find(range.min()));
+ REQUIRE(space.end() != space.find(range.max()));
+ }
+ CHECK(6 == space.count());
+ // Do the exact same networks again, should not change the range count.
+ for (auto &&[text, value] : r2) {
+ IPRange range{text};
+ space.blend(IPRange{text}, value, b2);
+ REQUIRE(space.end() != space.find(range.min()));
+ REQUIRE(space.end() != space.find(range.max()));
+ }
+ CHECK(6 == space.count());
+ // Verify that earlier ranges are still valid after the double blend.
+ for (auto &&[text, value] : r2) {
+ IPRange range{text};
+ REQUIRE(space.end() != space.find(range.min()));
+ REQUIRE(space.end() != space.find(range.max()));
+ }
+ // Color the non-intersecting range between ranges 1 and 2, verify coalesce.
+ space.blend(IPRange{"2001:4998:58:400::C/126"_tv}, 1, b2);
+ CHECK(5 == space.count());
+ // Verify all the data is in the ranges.
+ for (auto &&[text, value] : r2) {
+ IPRange range{text};
+ REQUIRE(space.end() != space.find(range.min()));
+ REQUIRE(space.end() != space.find(range.max()));
+ }
+
+ // Check some syntax.
+ {
+ auto a = IPAddr{"2001:4998:58:400::1E"};
+ auto [r, p] = *space.find(a);
+ REQUIRE_FALSE(r.empty());
+ REQUIRE(p == 1);
+ }
+ {
+ auto [r, p] = *space.find(IPAddr{"2001:4997:58:400::1E"});
+ REQUIRE(r.empty());
+ }
+
+ space.clear();
+ // Test a mix
+ unsigned idx = 0;
+ std::array<TextView, 6> mix_r{"1.1.1.1-1.1.1.111",
+ "2.2.2.2-2.2.2.222",
+ "3.3.3.3-3.255.255.255",
+ "1:2:3:4:5:6:7:8-1:2:3:4:5:6:7:ffff",
+ "11:2:3:4:5:6:7:8-11:2:3:4:5:6:7:ffff",
+ "111:2:3:4:5:6:7:8-111:2:3:4:5:6:7:ffff"};
+ for (auto &&r : mix_r) {
+ space.mark(IPRange(r), idx);
+ ++idx;
+ }
+
+ idx = 0;
+ std::string s;
+ for (auto [r, p] : space) {
+ REQUIRE(!r.empty());
+ REQUIRE(p == idx);
+ swoc::LocalBufferWriter<64> dbg;
+ bwformat(dbg, swoc::bwf::Spec::DEFAULT, r);
+ bwprint(s, "{}", r);
+ REQUIRE(s == mix_r[idx]);
+ ++idx;
+ }
+}
+
+TEST_CASE("IPSpace bitset", "[libswoc][ipspace][bitset]") {
+ using PAYLOAD = std::bitset<32>;
+ using Space = swoc::IPSpace<PAYLOAD>;
+
+ std::array<std::tuple<TextView, std::initializer_list<unsigned>>, 6> ranges = {
+ {{"172.28.56.12-172.28.56.99"_tv, {0, 2, 3}},
+ {"10.10.35.0/24"_tv, {1, 2}},
+ {"192.168.56.0/25"_tv, {10, 12, 31}},
+ {"1337::ded:beef-1337::ded:ceef"_tv, {4, 5, 6, 7}},
+ {"ffee:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"_tv, {9, 10, 18}},
+ {"10.12.148.0/23"_tv, {1, 2, 17}}}
+ };
+
+ Space space;
+
+ for (auto &&[text, bit_list] : ranges) {
+ PAYLOAD bits;
+ for (auto bit : bit_list) {
+ bits[bit] = true;
+ }
+ space.mark(IPRange{text}, bits);
+ }
+ REQUIRE(space.count() == ranges.size());
+
+ // Check that if an IPv4 lookup misses, it doesn't pass on to the first IPv6
+ auto [r1, p1] = *(space.find(IP4Addr{"172.28.56.100"}));
+ REQUIRE(true == r1.empty());
+ auto [r2, p2] = *(space.find(IPAddr{"172.28.56.100"}));
+ REQUIRE(true == r2.empty());
+}
+
+TEST_CASE("IPSpace docJJ", "[libswoc][ipspace][docJJ]") {
+ using PAYLOAD = std::bitset<32>;
+ using Space = swoc::IPSpace<PAYLOAD>;
+ // Add the bits in @rhs to the range.
+ auto blender = [](PAYLOAD &lhs, PAYLOAD const &rhs) -> bool {
+ lhs |= rhs;
+ return true;
+ };
+ // Add bit @a idx iff bits are already set.
+ auto additive = [](PAYLOAD &lhs, unsigned idx) -> bool {
+ if (!lhs.any()) {
+ return false;
+ }
+ lhs[idx] = true;
+ return true;
+ };
+
+ auto make_bits = [](std::initializer_list<unsigned> idx) -> PAYLOAD {
+ PAYLOAD bits;
+ for (auto bit : idx) {
+ bits[bit] = true;
+ }
+ return bits;
+ };
+
+ std::array<std::tuple<TextView, PAYLOAD>, 9> ranges = {
+ {{"100.0.0.0-100.0.0.255", make_bits({0})},
+ {"100.0.1.0-100.0.1.255", make_bits({1})},
+ {"100.0.2.0-100.0.2.255", make_bits({2})},
+ {"100.0.3.0-100.0.3.255", make_bits({3})},
+ {"100.0.4.0-100.0.4.255", make_bits({4})},
+ {"100.0.5.0-100.0.5.255", make_bits({5})},
+ {"100.0.6.0-100.0.6.255", make_bits({6})},
+ {"100.0.0.0-100.0.0.255", make_bits({31})},
+ {"100.0.1.0-100.0.1.255", make_bits({30})}}
+ };
+
+ static const std::array<PAYLOAD, 7> results = {make_bits({0, 31}), make_bits({1, 30}), make_bits({2}), make_bits({3}),
+ make_bits({4}), make_bits({5}), make_bits({6})};
+
+ Space space;
+
+ for (auto &&[text, bit_list] : ranges) {
+ space.blend(IPRange{text}, bit_list, blender);
+ }
+
+ // Check iteration - verify forward and reverse iteration yield the correct number of ranges
+ // and the range payloads match what is expected.
+ REQUIRE(space.count() == results.size());
+
+ unsigned idx;
+
+ idx = 0;
+ for (auto const &[range, bits] : space) {
+ CHECK(bits == results[idx]);
+ ++idx;
+ }
+
+ idx = 0;
+ for (auto spot = space.begin(); spot != space.end() && idx < results.size(); ++spot) {
+ auto const &[range, bits]{*spot};
+ CHECK(bits == results[idx]);
+ ++idx;
+ }
+
+ idx = results.size();
+ for (auto spot = space.end(); spot != space.begin();) {
+ auto const &[range, bits]{*--spot};
+ REQUIRE(idx > 0);
+ --idx;
+ CHECK(bits == results[idx]);
+ }
+
+ // Check iterator copying.
+ idx = 0;
+ Space::iterator iter;
+ IPRange range;
+ PAYLOAD bits;
+ for (auto spot = space.begin(); spot != space.end(); ++spot, ++idx) {
+ std::tie(range, bits) = spot->tuple();
+ CHECK(bits == results[idx]);
+ }
+
+ // This blend should change only existing ranges, not add range.
+ space.blend(IPRange{"99.128.0.0-100.0.1.255"}, 27, additive);
+ REQUIRE(space.count() == results.size()); // no more ranges.
+ // Verify first two ranges modified, but not the next.
+ REQUIRE(std::get<1>(*(space.find(IP4Addr{"100.0.0.37"}))) == make_bits({0, 27, 31}));
+ REQUIRE(std::get<1>(*(space.find(IP4Addr{"100.0.1.37"}))) == make_bits({1, 27, 30}));
+ REQUIRE(std::get<1>(*(space.find(IP4Addr{"100.0.2.37"}))) == make_bits({2}));
+
+ space.blend(IPRange{"100.10.1.1-100.10.2.2"}, make_bits({15}), blender);
+ REQUIRE(space.count() == results.size() + 1);
+ // Color in empty range - should not add range.
+ space.blend(IPRange{"100.8.10.25"}, 27, additive);
+ REQUIRE(space.count() == results.size() + 1);
+}
+
+TEST_CASE("IPSpace Edge", "[libswoc][ipspace][edge]") {
+ struct Thing {
+ unsigned _n;
+ Thing(Thing const &) = delete; // No copy.
+ Thing &operator=(Thing const &) = delete; // No self assignment.
+ bool
+ operator==(Thing const &that) const {
+ return _n == that._n;
+ }
+ };
+ using Space = IPSpace<Thing>;
+ Space space;
+
+ IP4Addr a1{"192.168.99.99"};
+ if (auto [r, p] = *(space.find(a1)); !r.empty()) {
+ REQUIRE(false); // Checking this syntax doesn't copy the payload.
+ }
+
+ auto const &cspace = space;
+ if (auto [r, p] = *(cspace.find(a1)); !r.empty()) {
+ Thing const &cp = p;
+ static_assert(std::is_const_v<typename std::remove_reference<decltype(cp)>::type>, "Payload was expected to be const.");
+ REQUIRE(false); // Checking this syntax doesn't copy the payload.
+ }
+ if (auto [r, p] = *(cspace.find(a1)); !r.empty()) {
+ static_assert(std::is_const_v<typename std::remove_reference<decltype(p)>::type>, "Payload was expected to be const.");
+ REQUIRE(false); // Checking this syntax doesn't copy the payload.
+ }
+
+ auto spot = cspace.find(a1);
+ static_assert(std::is_same_v<Space::const_iterator, decltype(spot)>);
+ auto &v1 = *spot;
+ auto &p1 = get<1>(v1);
+
+ if (auto &&[r, p] = *(cspace.find(a1)); !r.empty()) {
+ static_assert(std::is_same_v<swoc::IPRangeView const &, decltype(r)>);
+ IPRange rr = r;
+ swoc::IPRangeView rvv{r};
+ swoc::IPRangeView rv = r;
+ REQUIRE(rv == rr);
+ }
+}
+
+TEST_CASE("IPSpace Uthira", "[libswoc][ipspace][uthira]") {
+ struct Data {
+ TextView _pod;
+ int _rack = 0;
+ int _code = 0;
+
+ bool
+ operator==(Data const &that) const {
+ return _pod == that._pod && _rack == that._rack && _code == that._code;
+ }
+ };
+ auto pod_blender = [](Data &data, TextView const &p) {
+ data._pod = p;
+ return true;
+ };
+ auto rack_blender = [](Data &data, int r) {
+ data._rack = r;
+ return true;
+ };
+ auto code_blender = [](Data &data, int c) {
+ data._code = c;
+ return true;
+ };
+ swoc::IPSpace<Data> space;
+ // This is overkill, but no reason to not slam the code.
+ // For the original bug that triggered this testing, only the first line is actually necessary
+ // to cause the problem.
+ TextView content = R"(10.215.88.12-10.215.88.12,pdb,9
+ 10.215.88.13-10.215.88.13,pdb,9
+ 10.215.88.0-10.215.88.1,pdb,9
+ 10.215.88.2-10.215.88.3,pdb,9
+ 10.215.88.4-10.215.88.5,pdb,9
+ 10.215.88.6-10.215.88.7,pdb,9
+ 10.215.88.8-10.215.88.9,pdb,9
+ 10.215.88.10-10.215.88.11,pdb,9
+ 10.214.128.0-10.214.128.63,pda,1
+ 10.214.128.64-10.214.128.127,pda,1
+ 10.214.128.128-10.214.128.191,pda,1
+ 10.214.128.192-10.214.128.255,pda,1
+ 10.214.129.0-10.214.129.63,pda,1
+ 10.214.129.64-10.214.129.127,pda,1
+ 10.214.129.128-10.214.129.191,pda,1
+ 10.214.129.192-10.214.129.255,pda,1
+ 10.214.130.0-10.214.130.63,pda,1
+ 10.214.130.64-10.214.130.127,pda,1
+ 10.214.130.128-10.214.130.191,pda,1
+ 10.214.130.192-10.214.130.255,pda,1
+ 10.214.131.0-10.214.131.63,pda,1
+ 10.214.131.64-10.214.131.127,pda,1
+ 10.214.131.128-10.214.131.191,pda,1
+ 10.214.131.192-10.214.131.255,pda,1
+ 10.214.132.0-10.214.132.63,pda,1
+ 10.214.132.64-10.214.132.127,pda,1
+ 10.214.132.128-10.214.132.191,pda,1
+ 10.214.132.192-10.214.132.255,pda,1
+ 10.214.133.0-10.214.133.63,pda,1
+ 10.214.133.64-10.214.133.127,pda,1
+ 10.214.133.128-10.214.133.191,pda,1
+ 10.214.133.192-10.214.133.255,pda,1
+ 10.214.134.0-10.214.134.63,pda,1
+ 10.214.134.64-10.214.134.127,pda,1
+ 10.214.134.128-10.214.134.191,pda,1
+ 10.214.134.192-10.214.134.255,pda,1
+ 10.214.135.0-10.214.135.63,pda,1
+ 10.214.135.64-10.214.135.127,pda,1
+ 10.214.135.128-10.214.135.191,pda,1
+ 10.214.135.192-10.214.135.255,pda,1
+ 10.214.140.0-10.214.140.63,pda,1
+ 10.214.140.64-10.214.140.127,pda,1
+ 10.214.140.128-10.214.140.191,pda,1
+ 10.214.140.192-10.214.140.255,pda,1
+ 10.214.141.0-10.214.141.63,pda,1
+ 10.214.141.64-10.214.141.127,pda,1
+ 10.214.141.128-10.214.141.191,pda,1
+ 10.214.141.192-10.214.141.255,pda,1
+ 10.214.145.0-10.214.145.63,pda,1
+ 10.214.145.64-10.214.145.127,pda,1
+ 10.214.145.128-10.214.145.191,pda,1
+ 10.214.145.192-10.214.145.255,pda,1
+ 10.214.146.0-10.214.146.63,pda,1
+ 10.214.146.64-10.214.146.127,pda,1
+ 10.214.146.128-10.214.146.191,pda,1
+ 10.214.146.192-10.214.146.255,pda,1
+ 10.214.147.0-10.214.147.63,pda,1
+ 10.214.147.64-10.214.147.127,pda,1
+ 10.214.147.128-10.214.147.191,pda,1
+ 10.214.147.192-10.214.147.255,pda,1
+ 10.214.152.0-10.214.152.63,pda,1
+ 10.214.152.64-10.214.152.127,pda,1
+ 10.214.152.128-10.214.152.191,pda,1
+ 10.214.152.192-10.214.152.255,pda,1
+ 10.214.153.0-10.214.153.63,pda,1
+ 10.214.153.64-10.214.153.127,pda,1
+ 10.214.153.128-10.214.153.191,pda,1
+ 10.214.153.192-10.214.153.255,pda,1
+ 10.214.154.0-10.214.154.63,pda,1
+ 10.214.154.64-10.214.154.127,pda,1
+ 10.214.154.128-10.214.154.191,pda,1
+ 10.214.154.192-10.214.154.255,pda,1
+ 10.214.155.0-10.214.155.63,pda,1
+ 10.214.155.64-10.214.155.127,pda,1
+ 10.214.155.128-10.214.155.191,pda,1
+ 10.214.155.192-10.214.155.255,pda,1
+ 10.214.156.0-10.214.156.63,pda,1
+ 10.214.156.64-10.214.156.127,pda,1
+ 10.214.156.128-10.214.156.191,pda,1
+ 10.214.156.192-10.214.156.255,pda,1
+ 10.214.157.0-10.214.157.63,pda,1
+ 10.214.157.64-10.214.157.127,pda,1
+ 10.214.157.128-10.214.157.191,pda,1
+ 10.214.157.192-10.214.157.255,pda,1
+ 10.214.158.0-10.214.158.63,pda,1
+ 10.214.158.64-10.214.158.127,pda,1
+ 10.214.158.128-10.214.158.191,pda,1
+ 10.214.158.192-10.214.158.255,pda,1
+ 10.214.164.0-10.214.164.63,pda,1
+ 10.214.164.64-10.214.164.127,pda,1
+ 10.214.167.0-10.214.167.63,pda,1
+ 10.214.167.64-10.214.167.127,pda,1
+ 10.214.167.128-10.214.167.191,pda,1
+ 10.214.167.192-10.214.167.255,pda,1
+ 10.214.168.0-10.214.168.63,pda,1
+ 10.214.168.64-10.214.168.127,pda,1
+ 10.214.168.128-10.214.168.191,pda,1
+ 10.214.168.192-10.214.168.255,pda,1
+ 10.214.169.0-10.214.169.63,pda,1
+ 10.214.169.64-10.214.169.127,pda,1
+ 10.214.169.128-10.214.169.191,pda,1
+ 10.214.169.192-10.214.169.255,pda,1
+ 10.214.172.0-10.214.172.63,pda,1
+ 10.214.172.64-10.214.172.127,pda,1
+ 10.214.172.128-10.214.172.191,pda,1
+ 10.214.172.192-10.214.172.255,pda,1
+ 10.214.173.0-10.214.173.63,pda,1
+ 10.214.173.64-10.214.173.127,pda,1
+ 10.214.173.128-10.214.173.191,pda,1
+ 10.214.173.192-10.214.173.255,pda,1
+ 10.214.219.128-10.214.219.191,pda,1
+ 10.214.219.192-10.214.219.255,pda,1
+ 10.214.245.0-10.214.245.63,pda,1
+ 10.214.245.64-10.214.245.127,pda,1
+ 10.215.64.0-10.215.64.63,pda,1
+ 10.215.64.64-10.215.64.127,pda,1
+ 10.215.64.128-10.215.64.191,pda,1
+ 10.215.64.192-10.215.64.255,pda,1
+ 10.215.65.128-10.215.65.191,pda,1
+ 10.215.65.192-10.215.65.255,pda,1
+ 10.215.66.0-10.215.66.63,pda,1
+ 10.215.66.64-10.215.66.127,pda,1
+ 10.215.66.128-10.215.66.191,pda,1
+ 10.215.66.192-10.215.66.255,pda,1
+ 10.215.67.0-10.215.67.63,pda,1
+ 10.215.67.64-10.215.67.127,pda,1
+ 10.215.71.0-10.215.71.63,pda,1
+ 10.215.71.64-10.215.71.127,pda,1
+ 10.215.71.128-10.215.71.191,pda,1
+ 10.215.71.192-10.215.71.255,pda,1
+ 10.215.72.0-10.215.72.63,pda,1
+ 10.215.72.64-10.215.72.127,pda,1
+ 10.215.72.128-10.215.72.191,pda,1
+ 10.215.72.192-10.215.72.255,pda,1
+ 10.215.80.0-10.215.80.63,pda,1
+ 10.215.80.64-10.215.80.127,pda,1
+ 10.215.80.128-10.215.80.191,pda,1
+ 10.215.80.192-10.215.80.255,pda,1
+ 10.215.81.0-10.215.81.63,pda,1
+ 10.215.81.64-10.215.81.127,pda,1
+ 10.215.81.128-10.215.81.191,pda,1
+ 10.215.81.192-10.215.81.255,pda,1
+ 10.215.82.0-10.215.82.63,pda,1
+ 10.215.82.64-10.215.82.127,pda,1
+ 10.215.82.128-10.215.82.191,pda,1
+ 10.215.82.192-10.215.82.255,pda,1
+ 10.215.84.0-10.215.84.63,pda,1
+ 10.215.84.64-10.215.84.127,pda,1
+ 10.215.84.128-10.215.84.191,pda,1
+ 10.215.84.192-10.215.84.255,pda,1
+ 10.215.88.64-10.215.88.127,pdb,1
+ 10.215.88.128-10.215.88.191,pdb,1
+ 10.215.88.192-10.215.88.255,pdb,1
+ 10.215.89.0-10.215.89.63,pdb,1
+ 10.215.89.64-10.215.89.127,pdb,1
+ 10.215.89.128-10.215.89.191,pdb,1
+ 10.215.89.192-10.215.89.255,pdb,1
+ 10.215.90.0-10.215.90.63,pdb,1
+ 10.215.90.64-10.215.90.127,pdb,1
+ 10.215.90.128-10.215.90.191,pdb,1
+ 10.215.100.0-10.215.100.63,pda,1
+ 10.215.132.0-10.215.132.63,pda,1
+ 10.215.132.64-10.215.132.127,pda,1
+ 10.215.132.128-10.215.132.191,pda,1
+ 10.215.132.192-10.215.132.255,pda,1
+ 10.215.133.0-10.215.133.63,pda,1
+ 10.215.133.64-10.215.133.127,pda,1
+ 10.215.133.128-10.215.133.191,pda,1
+ 10.215.133.192-10.215.133.255,pda,1
+ 10.215.134.0-10.215.134.63,pda,1
+ 10.215.134.64-10.215.134.127,pda,1
+ 10.215.134.128-10.215.134.191,pda,1
+ 10.215.134.192-10.215.134.255,pda,1
+ 10.215.135.0-10.215.135.63,pda,1
+ 10.215.135.64-10.215.135.127,pda,1
+ 10.215.135.128-10.215.135.191,pda,1
+ 10.215.135.192-10.215.135.255,pda,1
+ 10.215.136.0-10.215.136.63,pda,1
+ 10.215.136.64-10.215.136.127,pda,1
+ 10.215.136.128-10.215.136.191,pda,1
+ 10.215.136.192-10.215.136.255,pda,1
+ 10.215.137.0-10.215.137.63,pda,1
+ 10.215.137.64-10.215.137.127,pda,1
+ 10.215.137.128-10.215.137.191,pda,1
+ 10.215.137.192-10.215.137.255,pda,1
+ 10.215.138.0-10.215.138.63,pda,1
+ 10.215.138.64-10.215.138.127,pda,1
+ 10.215.138.128-10.215.138.191,pda,1
+ 10.215.138.192-10.215.138.255,pda,1
+ 10.215.139.0-10.215.139.63,pda,1
+ 10.215.139.64-10.215.139.127,pda,1
+ 10.215.139.128-10.215.139.191,pda,1
+ 10.215.139.192-10.215.139.255,pda,1
+ 10.215.144.0-10.215.144.63,pda,1
+ 10.215.144.64-10.215.144.127,pda,1
+ 10.215.144.128-10.215.144.191,pda,1
+ 10.215.144.192-10.215.144.255,pda,1
+ 10.215.145.0-10.215.145.63,pda,1
+ 10.215.145.64-10.215.145.127,pda,1
+ 10.215.145.128-10.215.145.191,pda,1
+ 10.215.145.192-10.215.145.255,pda,1
+ 10.215.146.0-10.215.146.63,pda,1
+ 10.215.146.64-10.215.146.127,pda,1
+ 10.215.146.128-10.215.146.191,pda,1
+ 10.215.146.192-10.215.146.255,pda,1
+ 10.215.147.0-10.215.147.63,pda,1
+ 10.215.147.64-10.215.147.127,pda,1
+ 10.215.147.128-10.215.147.191,pda,1
+ 10.215.147.192-10.215.147.255,pda,1
+ 10.215.166.0-10.215.166.63,pda,1
+ 10.215.166.64-10.215.166.127,pda,1
+ 10.215.166.128-10.215.166.191,pda,1
+ 10.215.166.192-10.215.166.255,pda,1
+ 10.215.167.0-10.215.167.63,pda,1
+ 10.215.167.64-10.215.167.127,pda,1
+ 10.215.167.128-10.215.167.191,pda,1
+ 10.215.167.192-10.215.167.255,pda,1
+ 10.215.170.0-10.215.170.63,pda,1
+ 10.215.170.64-10.215.170.127,pda,1
+ 10.215.170.128-10.215.170.191,pda,1
+ 10.215.170.192-10.215.170.255,pda,1
+ 10.215.171.0-10.215.171.63,pda,1
+ 10.215.171.64-10.215.171.127,pda,1
+ 10.215.171.128-10.215.171.191,pda,1
+ 10.215.171.192-10.215.171.255,pda,1
+ 10.215.172.0-10.215.172.63,pda,1
+ 10.215.172.64-10.215.172.127,pda,1
+ 10.215.172.128-10.215.172.191,pda,1
+ 10.215.172.192-10.215.172.255,pda,1
+ 10.215.173.0-10.215.173.63,pda,1
+ 10.215.173.64-10.215.173.127,pda,1
+ 10.215.173.128-10.215.173.191,pda,1
+ 10.215.173.192-10.215.173.255,pda,1
+ 10.215.174.0-10.215.174.63,pda,1
+ 10.215.174.64-10.215.174.127,pda,1
+ 10.215.174.128-10.215.174.191,pda,1
+ 10.215.174.192-10.215.174.255,pda,1
+ 10.215.178.0-10.215.178.63,pda,1
+ 10.215.178.64-10.215.178.127,pda,1
+ 10.215.178.128-10.215.178.191,pda,1
+ 10.215.178.192-10.215.178.255,pda,1
+ 10.215.179.0-10.215.179.63,pda,1
+ 10.215.179.64-10.215.179.127,pda,1
+ 10.215.179.128-10.215.179.191,pda,1
+ 10.215.179.192-10.215.179.255,pda,1
+ 10.215.192.0-10.215.192.63,pda,1
+ 10.215.192.64-10.215.192.127,pda,1
+ 10.215.192.128-10.215.192.191,pda,1
+ 10.215.192.192-10.215.192.255,pda,1
+ 10.215.193.0-10.215.193.63,pda,1
+ 10.215.193.64-10.215.193.127,pda,1
+ 10.215.193.128-10.215.193.191,pda,1
+ 10.215.193.192-10.215.193.255,pda,1
+ 10.215.194.0-10.215.194.63,pda,1
+ 10.215.194.64-10.215.194.127,pda,1
+ 10.215.194.128-10.215.194.191,pda,1
+ 10.215.194.192-10.215.194.255,pda,1
+ 10.215.195.0-10.215.195.63,pda,1
+ 10.215.195.64-10.215.195.127,pda,1
+ 10.215.195.128-10.215.195.191,pda,1
+ 10.215.195.192-10.215.195.255,pda,1
+ 10.215.196.0-10.215.196.63,pda,1
+ 10.215.196.64-10.215.196.127,pda,1
+ 10.215.196.128-10.215.196.191,pda,1
+ 10.215.196.192-10.215.196.255,pda,1
+ 10.215.197.0-10.215.197.63,pda,1
+ 10.215.197.64-10.215.197.127,pda,1
+ 10.215.197.128-10.215.197.191,pda,1
+ 10.215.197.192-10.215.197.255,pda,1
+ 10.215.198.0-10.215.198.63,pda,1
+ 10.215.198.64-10.215.198.127,pda,1
+ 10.215.198.128-10.215.198.191,pda,1
+ 10.215.198.192-10.215.198.255,pda,1
+ 10.215.199.0-10.215.199.63,pda,1
+ 10.215.199.64-10.215.199.127,pda,1
+ 10.215.199.128-10.215.199.191,pda,1
+ 10.215.199.192-10.215.199.255,pda,1
+ 10.215.200.0-10.215.200.63,pda,1
+ 10.215.200.64-10.215.200.127,pda,1
+ 10.215.200.128-10.215.200.191,pda,1
+ 10.215.200.192-10.215.200.255,pda,1
+ 10.215.201.0-10.215.201.63,pda,1
+ 10.215.201.64-10.215.201.127,pda,1
+ 10.215.201.128-10.215.201.191,pda,1
+ 10.215.201.192-10.215.201.255,pda,1
+ 10.215.202.0-10.215.202.63,pda,1
+ 10.215.202.64-10.215.202.127,pda,1
+ 10.215.202.128-10.215.202.191,pda,1
+ 10.215.202.192-10.215.202.255,pda,1
+ 10.215.203.0-10.215.203.63,pda,1
+ 10.215.203.64-10.215.203.127,pda,1
+ 10.215.203.128-10.215.203.191,pda,1
+ 10.215.203.192-10.215.203.255,pda,1
+ 10.215.204.0-10.215.204.63,pda,1
+ 10.215.204.64-10.215.204.127,pda,1
+ 10.215.204.128-10.215.204.191,pda,1
+ 10.215.204.192-10.215.204.255,pda,1
+ 10.215.205.0-10.215.205.63,pda,1
+ 10.215.205.64-10.215.205.127,pda,1
+ 10.215.205.128-10.215.205.191,pda,1
+ 10.215.205.192-10.215.205.255,pda,1
+ 10.215.206.0-10.215.206.63,pda,1
+ 10.215.206.64-10.215.206.127,pda,1
+ 10.215.206.128-10.215.206.191,pda,1
+ 10.215.206.192-10.215.206.255,pda,1
+ 10.215.207.0-10.215.207.63,pda,1
+ 10.215.207.64-10.215.207.127,pda,1
+ 10.215.207.128-10.215.207.191,pda,1
+ 10.215.207.192-10.215.207.255,pda,1
+ 10.215.208.0-10.215.208.63,pda,1
+ 10.215.208.64-10.215.208.127,pda,1
+ 10.215.208.128-10.215.208.191,pda,1
+ 10.215.208.192-10.215.208.255,pda,1
+ 10.215.209.0-10.215.209.63,pda,1
+ 10.215.209.64-10.215.209.127,pda,1
+ 10.215.209.128-10.215.209.191,pda,1
+ 10.215.209.192-10.215.209.255,pda,1
+ 10.215.210.0-10.215.210.63,pda,1
+ 10.215.210.64-10.215.210.127,pda,1
+ 10.215.210.128-10.215.210.191,pda,1
+ 10.215.210.192-10.215.210.255,pda,1
+ 10.215.211.0-10.215.211.63,pda,1
+ 10.215.211.64-10.215.211.127,pda,1
+ 10.215.211.128-10.215.211.191,pda,1
+ 10.215.211.192-10.215.211.255,pda,1
+ 10.215.212.0-10.215.212.63,pda,1
+ 10.215.212.64-10.215.212.127,pda,1
+ 10.215.212.128-10.215.212.191,pda,1
+ 10.215.212.192-10.215.212.255,pda,1
+ 10.215.213.0-10.215.213.63,pda,1
+ 10.215.213.64-10.215.213.127,pda,1
+ 10.215.213.128-10.215.213.191,pda,1
+ 10.215.213.192-10.215.213.255,pda,1
+ 10.215.214.0-10.215.214.63,pda,1
+ 10.215.214.64-10.215.214.127,pda,1
+ 10.215.214.128-10.215.214.191,pda,1
+ 10.215.214.192-10.215.214.255,pda,1
+ 10.215.215.0-10.215.215.63,pda,1
+ 10.215.215.64-10.215.215.127,pda,1
+ 10.215.215.128-10.215.215.191,pda,1
+ 10.215.215.192-10.215.215.255,pda,1
+ 10.215.216.0-10.215.216.63,pda,1
+ 10.215.216.64-10.215.216.127,pda,1
+ 10.215.216.128-10.215.216.191,pda,1
+ 10.215.216.192-10.215.216.255,pda,1
+ 10.215.217.0-10.215.217.63,pda,1
+ 10.215.217.64-10.215.217.127,pda,1
+ 10.215.217.128-10.215.217.191,pda,1
+ 10.215.217.192-10.215.217.255,pda,1
+ 10.215.218.0-10.215.218.63,pda,1
+ 10.215.218.64-10.215.218.127,pda,1
+ 10.215.218.128-10.215.218.191,pda,1
+ 10.215.218.192-10.215.218.255,pda,1
+ 10.215.219.0-10.215.219.63,pda,1
+ 10.215.219.64-10.215.219.127,pda,1
+ 10.215.219.128-10.215.219.191,pda,1
+ 10.215.219.192-10.215.219.255,pda,1
+ 10.215.220.0-10.215.220.63,pda,1
+ 10.215.220.64-10.215.220.127,pda,1
+ 10.215.220.128-10.215.220.191,pda,1
+ 10.215.220.192-10.215.220.255,pda,1
+ 10.215.221.0-10.215.221.63,pda,1
+ 10.215.221.64-10.215.221.127,pda,1
+ 10.215.221.128-10.215.221.191,pda,1
+ 10.215.221.192-10.215.221.255,pda,1
+ 10.215.222.0-10.215.222.63,pda,1
+ 10.215.222.64-10.215.222.127,pda,1
+ 10.215.222.128-10.215.222.191,pda,1
+ 10.215.222.192-10.215.222.255,pda,1
+ 10.215.223.0-10.215.223.63,pda,1
+ 10.215.223.64-10.215.223.127,pda,1
+ 10.215.223.128-10.215.223.191,pda,1
+ 10.215.223.192-10.215.223.255,pda,1
+ 10.215.224.0-10.215.224.63,pda,1
+ 10.215.224.64-10.215.224.127,pda,1
+ 10.215.224.128-10.215.224.191,pda,1
+ 10.215.224.192-10.215.224.255,pda,1
+ 10.215.225.0-10.215.225.63,pda,1
+ 10.215.225.64-10.215.225.127,pda,1
+ 10.215.225.128-10.215.225.191,pda,1
+ 10.215.225.192-10.215.225.255,pda,1
+ 10.215.226.0-10.215.226.63,pda,1
+ 10.215.226.64-10.215.226.127,pda,1
+ 10.215.226.128-10.215.226.191,pda,1
+ 10.215.226.192-10.215.226.255,pda,1
+ 10.215.227.0-10.215.227.63,pda,1
+ 10.215.227.64-10.215.227.127,pda,1
+ 10.215.227.128-10.215.227.191,pda,1
+ 10.215.227.192-10.215.227.255,pda,1
+ 10.215.228.0-10.215.228.63,pda,1
+ 10.215.228.64-10.215.228.127,pda,1
+ 10.215.228.128-10.215.228.191,pda,1
+ 10.215.228.192-10.215.228.255,pda,1
+ 10.215.229.0-10.215.229.63,pda,1
+ 10.215.229.64-10.215.229.127,pda,1
+ 10.215.229.128-10.215.229.191,pda,1
+ 10.215.229.192-10.215.229.255,pda,1
+ 10.215.230.0-10.215.230.63,pda,1
+ 10.215.230.64-10.215.230.127,pda,1
+ 10.215.230.128-10.215.230.191,pda,1
+ 10.215.230.192-10.215.230.255,pda,1
+ 10.215.231.0-10.215.231.63,pda,1
+ 10.215.231.64-10.215.231.127,pda,1
+ 10.215.231.128-10.215.231.191,pda,1
+ 10.215.231.192-10.215.231.255,pda,1
+ 10.215.232.0-10.215.232.63,pda,1
+ 10.215.232.64-10.215.232.127,pda,1
+ 10.215.232.128-10.215.232.191,pda,1
+ 10.215.232.192-10.215.232.255,pda,1
+ 10.215.233.0-10.215.233.63,pda,1
+ 10.215.233.64-10.215.233.127,pda,1
+ 10.215.233.128-10.215.233.191,pda,1
+ 10.215.233.192-10.215.233.255,pda,1
+ 10.215.234.0-10.215.234.63,pda,1
+ 10.215.234.64-10.215.234.127,pda,1
+ 10.215.234.128-10.215.234.191,pda,1
+ 10.215.234.192-10.215.234.255,pda,1
+ 10.215.235.0-10.215.235.63,pda,1
+ 10.215.235.64-10.215.235.127,pda,1
+ 10.215.235.128-10.215.235.191,pda,1
+ 10.215.235.192-10.215.235.255,pda,1
+ 10.215.236.0-10.215.236.63,pda,1
+ 10.215.236.64-10.215.236.127,pda,1
+ 10.215.236.128-10.215.236.191,pda,1
+ 10.215.236.192-10.215.236.255,pda,1
+ 10.215.237.0-10.215.237.63,pda,1
+ 10.215.237.64-10.215.237.127,pda,1
+ 10.215.237.128-10.215.237.191,pda,1
+ 10.215.237.192-10.215.237.255,pda,1
+ 10.215.238.0-10.215.238.63,pda,1
+ 10.215.238.64-10.215.238.127,pda,1
+ 10.215.238.128-10.215.238.191,pda,1
+ 10.215.238.192-10.215.238.255,pda,1
+ 10.215.239.0-10.215.239.63,pda,1
+ 10.215.239.64-10.215.239.127,pda,1
+ 10.215.239.128-10.215.239.191,pda,1
+ 10.215.239.192-10.215.239.255,pda,1
+ 10.215.240.0-10.215.240.63,pda,1
+ 10.215.240.64-10.215.240.127,pda,1
+ 10.215.240.128-10.215.240.191,pda,1
+ 10.215.240.192-10.215.240.255,pda,1
+ 10.215.241.0-10.215.241.63,pda,1
+ 10.215.241.64-10.215.241.127,pda,1
+ 10.215.241.128-10.215.241.191,pda,1
+ 10.215.241.192-10.215.241.255,pda,1
+ 10.215.242.0-10.215.242.63,pda,1
+ 10.215.242.64-10.215.242.127,pda,1
+ 10.215.242.128-10.215.242.191,pda,1
+ 10.215.242.192-10.215.242.255,pda,1
+ 10.215.243.0-10.215.243.63,pda,1
+ 10.215.243.64-10.215.243.127,pda,1
+ 10.215.243.128-10.215.243.191,pda,1
+ 10.215.243.192-10.215.243.255,pda,1
+ 10.215.244.0-10.215.244.63,pda,1
+ 10.215.244.64-10.215.244.127,pda,1
+ 10.215.244.128-10.215.244.191,pda,1
+ 10.215.244.192-10.215.244.255,pda,1
+ 10.215.245.0-10.215.245.63,pda,1
+ 10.215.245.64-10.215.245.127,pda,1
+ 10.215.245.128-10.215.245.191,pda,1
+ 10.215.245.192-10.215.245.255,pda,1
+ 10.215.246.0-10.215.246.63,pda,1
+ 10.215.246.64-10.215.246.127,pda,1
+ 10.215.246.128-10.215.246.191,pda,1
+ 10.215.246.192-10.215.246.255,pda,1
+ 10.215.247.0-10.215.247.63,pda,1
+ 10.215.247.64-10.215.247.127,pda,1
+ 10.215.247.128-10.215.247.191,pda,1
+ 10.215.247.192-10.215.247.255,pda,1
+ 10.215.248.0-10.215.248.63,pda,1
+ 10.215.248.64-10.215.248.127,pda,1
+ 10.215.248.128-10.215.248.191,pda,1
+ 10.215.248.192-10.215.248.255,pda,1
+ 10.215.249.0-10.215.249.63,pda,1
+ 10.215.249.64-10.215.249.127,pda,1
+ 10.215.249.128-10.215.249.191,pda,1
+ 10.215.249.192-10.215.249.255,pda,1
+ 10.215.250.0-10.215.250.63,pda,1
+ 10.215.250.64-10.215.250.127,pda,1
+ 10.215.250.128-10.215.250.191,pda,1
+ 10.215.250.192-10.215.250.255,pda,1
+ 10.215.251.0-10.215.251.63,pda,1
+ 10.215.251.64-10.215.251.127,pda,1
+ 10.215.251.128-10.215.251.191,pda,1
+ 10.215.251.192-10.215.251.255,pda,1
+ 10.215.252.0-10.215.252.63,pda,1
+ 10.215.252.64-10.215.252.127,pda,1
+ 10.215.252.128-10.215.252.191,pda,1
+ 10.215.252.192-10.215.252.255,pda,1
+ 10.215.253.0-10.215.253.63,pda,1
+ 10.215.253.64-10.215.253.127,pda,1
+ 10.215.253.128-10.215.253.191,pda,1
+ 10.215.253.192-10.215.253.255,pda,1
+ 10.215.254.0-10.215.254.63,pda,1
+ 10.215.254.64-10.215.254.127,pda,1
+ 10.215.254.128-10.215.254.191,pda,1
+ 10.215.254.192-10.215.254.255,pda,1
+ 10.215.255.0-10.215.255.63,pda,1
+ 10.215.255.64-10.215.255.127,pda,1
+ 10.215.255.128-10.215.255.191,pda,1
+ 10.215.255.192-10.215.255.255,pda,1
+ 10.214.164.128-10.214.164.255,pda,1
+ 10.214.219.0-10.214.219.127,pda,1
+ 10.214.245.128-10.214.245.255,pda,1
+ 10.215.65.0-10.215.65.127,pda,1
+ 10.215.67.128-10.215.67.255,pda,1
+ 10.215.73.0-10.215.73.127,pda,1
+ 10.215.73.128-10.215.73.255,pda,1
+ 10.215.78.0-10.215.78.127,pda,1
+ 10.215.78.128-10.215.78.255,pda,1
+ 10.215.79.0-10.215.79.127,pda,1
+ 10.215.79.128-10.215.79.255,pda,1
+ 10.214.136.0-10.214.136.255,pda,1
+ 10.214.137.0-10.214.137.255,pda,1
+ 10.214.138.0-10.214.138.255,pda,1
+ 10.214.139.0-10.214.139.255,pda,1
+ 10.214.142.0-10.214.142.255,pda,1
+ 10.214.143.0-10.214.143.255,pda,1
+ 10.214.144.0-10.214.144.255,pda,1
+ 10.214.159.0-10.214.159.255,pda,1
+ 10.214.160.0-10.214.160.255,pda,1
+ 10.214.161.0-10.214.161.255,pda,1
+ 10.214.162.0-10.214.162.255,pda,1
+ 10.214.163.0-10.214.163.255,pda,1
+ 10.214.165.0-10.214.165.255,pda,1
+ 10.214.166.0-10.214.166.255,pda,1
+ 10.214.170.0-10.214.170.255,pda,1
+ 10.214.171.0-10.214.171.255,pda,1
+ 10.214.218.0-10.214.218.255,pda,1
+ 10.214.244.0-10.214.244.255,pda,1
+ 10.215.70.0-10.215.70.255,pda,1
+ 10.215.83.0-10.215.83.255,pda,1
+ 10.215.85.0-10.215.85.255,pda,1
+ 10.215.101.0-10.215.101.255,pda,1
+ 10.215.104.0-10.215.104.255,pda,1
+ 10.215.164.0-10.215.164.255,pda,1
+ 10.215.165.0-10.215.165.255,pda,1
+ 10.215.175.0-10.215.175.255,pda,1
+ 10.214.148.0-10.214.149.255,pda,1
+ 10.214.150.0-10.214.151.255,pda,1
+ 10.214.174.0-10.214.175.255,pda,1
+ 10.214.216.0-10.214.217.255,pda,1
+ 10.214.246.0-10.214.247.255,pda,1
+ 10.215.68.0-10.215.69.255,pda,1
+ 10.215.74.0-10.215.75.255,pda,1
+ 10.215.76.0-10.215.77.255,pda,1
+ 10.215.96.0-10.215.97.255,pda,1
+ 10.215.98.0-10.215.99.255,pda,1
+ 10.215.102.0-10.215.103.255,pda,1
+ 10.215.140.0-10.215.141.255,pda,1
+ 10.215.142.0-10.215.143.255,pda,1
+ 10.215.148.0-10.215.149.255,pda,1
+ 10.215.150.0-10.215.151.255,pda,1
+ 10.215.152.0-10.215.153.255,pda,1
+ 10.215.154.0-10.215.155.255,pda,1
+ 10.215.168.0-10.215.169.255,pda,1
+ 10.215.176.0-10.215.177.255,pda,1
+ 10.214.220.0-10.214.223.255,pda,1
+ 10.214.240.0-10.214.243.255,pda,1
+ 10.215.108.0-10.215.111.255,pda,1
+ 10.215.128.0-10.215.131.255,pda,1
+ 10.215.156.0-10.215.159.255,pda,1
+ 10.215.160.0-10.215.163.255,pda,1
+ 10.215.180.0-10.215.183.255,pda,1
+ 10.214.208.0-10.214.215.255,pda,1
+ 10.214.248.0-10.214.255.255,pda,1
+ 10.215.184.0-10.215.191.255,pda,1
+ 10.214.176.0-10.214.191.255,pda,1
+ 10.214.192.0-10.214.207.255,pda,1
+ 10.214.224.0-10.214.239.255,pda,1
+ 10.215.112.0-10.215.127.255,pda,1
+ 10.215.32.0-10.215.63.255,pda,9
+ 10.214.0.0-10.214.127.255,pda,9
+ )";
+
+ // Need to have the working ranges covered first, before they're blended.
+ space.blend(IP4Range{"10.214.0.0/15"}, 1, code_blender);
+ // Now blend the working ranges over the base range.
+ while (content) {
+ auto line = content.take_prefix_at('\n').trim_if(&isspace);
+ if (line.empty()) {
+ continue;
+ }
+ IP4Range range{line.take_prefix_at(',')};
+ auto pod = line.take_prefix_at(',');
+ int r = swoc::svtoi(line.take_prefix_at(','));
+ space.blend(range, pod, pod_blender);
+ space.blend(range, r, rack_blender);
+ if (space.count() > 2) {
+ auto spot = space.begin();
+ auto [r1, p1] = *++spot;
+ auto [r2, p2] = *++spot;
+ REQUIRE(r1.max() < r2.min()); // This is supposed to be an invariant! Make sure.
+ auto back = space.end();
+ auto [br1, bp1] = *--back;
+ auto [br2, bp2] = *--back;
+ REQUIRE(br2.max() < br1.min()); // This is supposed to be an invariant! Make sure.
+ }
+ }
+
+ // Do some range intersection checks.
+}
+
+TEST_CASE("IPSpace skew overlap blend", "[libswoc][ipspace][blend][skew]") {
+ std::string buff;
+ enum class Pod { INVALID, zio, zaz, zlz };
+ swoc::Lexicon<Pod> PodNames{
+ {{Pod::zio, "zio"}, {Pod::zaz, "zaz"}, {Pod::zlz, "zlz"}},
+ "-1"
+ };
+
+ struct Data {
+ int _state = 0;
+ int _country = -1;
+ int _rack = 0;
+ Pod _pod = Pod::INVALID;
+ int _code = 0;
+
+ bool
+ operator==(Data const &that) const {
+ return _pod == that._pod && _rack == that._rack && _code == that._code && _state == that._state && _country == that._country;
+ }
+ };
+
+ using Src_1 = std::tuple<int, Pod, int>; // rack, pod, code
+ using Src_2 = std::tuple<int, int>; // state, country.
+ auto blend_1 = [](Data &data, Src_1 const &src) {
+ std::tie(data._rack, data._pod, data._code) = src;
+ return true;
+ };
+ [[maybe_unused]] auto blend_2 = [](Data &data, Src_2 const &src) {
+ std::tie(data._state, data._country) = src;
+ return true;
+ };
+ swoc::IPSpace<Data> space;
+ space.blend(IPRange("14.6.128.0-14.6.191.255"), Src_2{32, 231}, blend_2);
+ space.blend(IPRange("14.6.192.0-14.6.223.255"), Src_2{32, 231}, blend_2);
+ REQUIRE(space.count() == 1);
+ space.blend(IPRange("14.6.160.0-14.6.160.1"), Src_1{1, Pod::zaz, 1}, blend_1);
+ REQUIRE(space.count() == 3);
+ space.blend(IPRange("14.6.160.64-14.6.160.95"), Src_1{1, Pod::zio, 1}, blend_1);
+ space.blend(IPRange("14.6.160.96-14.6.160.127"), Src_1{1, Pod::zlz, 1}, blend_1);
+ space.blend(IPRange("14.6.160.128-14.6.160.255"), Src_1{1, Pod::zlz, 1}, blend_1);
+ space.blend(IPRange("14.6.0.0-14.6.127.255"), Src_2{32, 231}, blend_2);
+
+ std::array<std::tuple<IPRange, Data>, 6> results = {
+ {{IPRange("14.6.0.0-14.6.159.255"), Data{32, 231, 0, Pod::INVALID, 0}},
+ {IPRange("14.6.160.0-14.6.160.1"), Data{32, 231, 1, Pod::zaz, 1}},
+ {IPRange("14.6.160.2-14.6.160.63"), Data{32, 231, 0, Pod::INVALID, 0}},
+ {IPRange("14.6.160.64-14.6.160.95"), Data{32, 231, 1, Pod::zio, 1}},
+ {IPRange("14.6.160.96-14.6.160.255"), Data{32, 231, 1, Pod::zlz, 1}},
+ {IPRange("14.6.161.0-14.6.223.255"), Data{32, 231, 0, Pod::INVALID, 0}}}
+ };
+ REQUIRE(space.count() == results.size());
+ unsigned idx = 0;
+ for (auto const &v : space) {
+ REQUIRE(v == results[idx]);
+ ++idx;
+ }
+}
+
+TEST_CASE("IPSpace fill", "[libswoc][ipspace][fill]") {
+ using PAYLOAD = unsigned;
+ using Space = swoc::IPSpace<PAYLOAD>;
+
+ std::array<std::tuple<TextView, unsigned>, 6> ranges{
+ {{"172.28.56.12-172.28.56.99"_tv, 1},
+ {"10.10.35.0/24"_tv, 2},
+ {"192.168.56.0/25"_tv, 3},
+ {"1337::ded:beef-1337::ded:ceef"_tv, 4},
+ {"ffee:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"_tv, 5},
+ {"10.12.148.0/23"_tv, 6}}
+ };
+
+ Space space;
+
+ for (auto &&[text, v] : ranges) {
+ space.fill(IPRange{text}, v);
+ }
+ REQUIRE(space.count() == ranges.size());
+
+ auto [r1, p1] = *(space.find(IP4Addr{"172.28.56.100"}));
+ REQUIRE(r1.empty());
+ auto [r2, p2] = *(space.find(IPAddr{"172.28.56.87"}));
+ REQUIRE_FALSE(r2.empty());
+
+ space.fill(IPRange{"10.0.0.0/8"}, 7);
+ REQUIRE(space.count() == ranges.size() + 3);
+ space.fill(IPRange{"9.0.0.0-11.255.255.255"}, 7);
+ REQUIRE(space.count() == ranges.size() + 3);
+
+ {
+ auto [r, p] = *(space.find(IPAddr{"10.99.88.77"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 7);
+ }
+
+ {
+ auto [r, p] = *(space.find(IPAddr{"10.10.35.35"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 2);
+ }
+
+ {
+ auto [r, p] = *(space.find(IPAddr{"192.168.56.56"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 3);
+ }
+
+ {
+ auto [r, p] = *(space.find(IPAddr{"11.11.11.11"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 7);
+ }
+
+ space.fill(IPRange{"192.168.56.0-192.168.56.199"}, 8);
+ REQUIRE(space.count() == ranges.size() + 4);
+ {
+ auto [r, p] = *(space.find(IPAddr{"192.168.55.255"}));
+ REQUIRE(true == r.empty());
+ }
+ {
+ auto [r, p] = *(space.find(IPAddr{"192.168.56.0"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 3);
+ }
+ {
+ auto [r, p] = *(space.find(IPAddr{"192.168.56.128"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 8);
+ }
+
+ space.fill(IPRange{"0.0.0.0/0"}, 0);
+ {
+ auto [r, p] = *(space.find(IPAddr{"192.168.55.255"}));
+ REQUIRE(false == r.empty());
+ REQUIRE(p == 0);
+ }
+}
+
+TEST_CASE("IPSpace intersect", "[libswoc][ipspace][intersect]") {
+ std::string dbg;
+ using PAYLOAD = unsigned;
+ using Space = swoc::IPSpace<PAYLOAD>;
+
+ std::array<std::tuple<TextView, unsigned>, 7> ranges{
+ {{"172.28.56.12-172.28.56.99"_tv, 1},
+ {"10.10.35.0/24"_tv, 2},
+ {"192.168.56.0/25"_tv, 3},
+ {"10.12.148.0/23"_tv, 6},
+ {"10.14.56.0/24"_tv, 9},
+ {"192.168.57.0/25"_tv, 7},
+ {"192.168.58.0/25"_tv, 5}}
+ };
+
+ Space space;
+
+ for (auto &&[text, v] : ranges) {
+ space.fill(IPRange{text}, v);
+ }
+
+ {
+ IPRange r{"172.0.0.0/16"};
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(begin == end);
+ }
+ {
+ IPRange r{"172.0.0.0/8"};
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 1);
+ }
+ {
+ IPRange r{"10.0.0.0/8"};
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 3);
+ }
+ {
+ IPRange r{"10.10.35.17-10.12.148.7"};
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 2);
+ }
+ {
+ IPRange r{"10.10.35.0-10.14.56.0"};
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 3);
+ }
+ {
+ IPRange r{"10.13.0.0-10.15.148.7"}; // past the end
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 1);
+ }
+ {
+ IPRange r{"10.13.0.0-10.14.55.127"}; // inside a gap.
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(begin == end);
+ }
+ {
+ IPRange r{"192.168.56.127-192.168.67.35"}; // include last range.
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 3);
+ }
+ {
+ IPRange r{"192.168.57.128-192.168.67.35"}; // only last range.
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 1);
+ }
+ {
+ IPRange r{"192.168.57.128-192.168.58.10"}; // only last range.
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 1);
+ }
+ {
+ IPRange r{"192.168.50.0-192.168.57.35"}; // include last range.
+ auto &&[begin, end] = space.intersection(r);
+ REQUIRE(std::distance(begin, end) == 2);
+ }
+}
+
+TEST_CASE("IPSrv", "[libswoc][IPSrv]") {
+ using swoc::IP4Srv;
+ using swoc::IP6Srv;
+ using swoc::IPSrv;
+
+ IP4Srv s4;
+ IP6Srv s6;
+ IPSrv s;
+
+ IP4Addr a1{"192.168.34.56"};
+ IP4Addr a2{"10.9.8.7"};
+ IP6Addr aa1{"ffee:1f2d:c587:24c3:9128:3349:3cee:143"};
+
+ s6.assign(aa1, 99);
+ REQUIRE(s6.addr() == aa1);
+ REQUIRE(s6.host_order_port() == 99);
+ REQUIRE(s6 == IP6Srv(aa1, 99));
+
+ // Test various constructors.
+ s4.assign(a2, 88);
+ IP4Addr tmp1{s4.addr()};
+ REQUIRE(s4 == tmp1);
+ IP4Addr tmp2 = s4;
+ REQUIRE(s4 == tmp2);
+ IP4Addr tmp3{s4};
+ REQUIRE(s4 == tmp3);
+ REQUIRE(s4.addr() == tmp3); // double check equality.
+
+ IP4Srv s4_1{"10.9.8.7:56"};
+ REQUIRE(s4_1.host_order_port() == 56);
+ REQUIRE(s4_1 == a2);
+ CHECK(s4_1.load("10.2:56"));
+ CHECK_FALSE(s4_1.load("10.1.2.3.567899"));
+ CHECK_FALSE(s4_1.load("10.1.2.3.56f"));
+ CHECK_FALSE(s4_1.load("10.1.2.56f"));
+ CHECK(s4_1.load("10.1.2.3"));
+ REQUIRE(s4_1.host_order_port() == 0);
+
+ CHECK(s6.load("[ffee:1f2d:c587:24c3:9128:3349:3cee:143]:956"));
+ REQUIRE(s6 == aa1);
+ REQUIRE(s6.host_order_port() == 956);
+ CHECK(s6.load("ffee:1f2d:c587:24c3:9128:3349:3cee:143"));
+ REQUIRE(s6 == aa1);
+ REQUIRE(s6.host_order_port() == 0);
+
+ CHECK(s.load("[ffee:1f2d:c587:24c3:9128:3349:3cee:143]:956"));
+ REQUIRE(s == aa1);
+ REQUIRE(s.host_order_port() == 956);
+ CHECK(s.load("ffee:1f2d:c587:24c3:9128:3349:3cee:143"));
+ REQUIRE(s == aa1);
+ REQUIRE(s.host_order_port() == 0);
+}
+
+TEST_CASE("IPRangeSet", "[libswoc][iprangeset]") {
+ std::array<TextView, 6> ranges = {"172.28.56.12-172.28.56.99"_tv,
+ "10.10.35.0/24"_tv,
+ "192.168.56.0/25"_tv,
+ "1337::ded:beef-1337::ded:ceef"_tv,
+ "ffee:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"_tv,
+ "10.12.148.0/23"_tv};
+
+ IPRangeSet addrs;
+
+ for (auto rtxt : ranges) {
+ IPRange r{rtxt};
+ addrs.mark(r);
+ }
+
+ unsigned n = 0;
+ bool valid_p = true;
+ for (auto r : addrs) {
+ valid_p = valid_p && !r.empty();
+ ++n;
+ }
+ REQUIRE(n == addrs.count());
+ REQUIRE(valid_p);
+}
diff --git a/lib/swoc/unit_tests/test_meta.cc b/lib/swoc/unit_tests/test_meta.cc
new file mode 100644
index 0000000000..d0d6ec5985
--- /dev/null
+++ b/lib/swoc/unit_tests/test_meta.cc
@@ -0,0 +1,119 @@
+/** @file
+
+ Unit tests for ts_meta.h and other meta programming.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include <cstring>
+#include <variant>
+
+#include "swoc/swoc_meta.h"
+#include "swoc/TextView.h"
+
+#include "catch.hpp"
+
+using swoc::TextView;
+using namespace swoc::literals;
+
+struct A {
+ int _value;
+};
+
+struct AA : public A {};
+
+struct B {
+ std::string _value;
+};
+
+struct C {};
+
+struct D {};
+
+TEST_CASE("Meta Example", "[meta][example]") {
+ REQUIRE(true == swoc::meta::is_any_of<A, A, B, C>::value);
+ REQUIRE(false == swoc::meta::is_any_of<D, A, B, C>::value);
+ REQUIRE(true == swoc::meta::is_any_of<A, A>::value);
+ REQUIRE(false == swoc::meta::is_any_of<A, D>::value);
+ REQUIRE(false == swoc::meta::is_any_of<A>::value); // verify degenerate use case.
+
+ REQUIRE(true == swoc::meta::is_any_of_v<A, A, B, C>);
+ REQUIRE(false == swoc::meta::is_any_of_v<D, A, B, C>);
+}
+
+// Start of ts::meta testing.
+
+namespace {
+template <typename T>
+auto
+detect(T &&t, swoc::meta::CaseTag<0>) -> std::string_view {
+ return "none";
+}
+template <typename T>
+auto
+detect(T &&t, swoc::meta::CaseTag<1>) -> decltype(t._value, std::string_view()) {
+ return "value";
+}
+template <typename T>
+std::string_view
+detect(T &&t) {
+ return detect(t, swoc::meta::CaseArg);
+}
+} // namespace
+
+TEST_CASE("Meta", "[meta]") {
+ REQUIRE(detect(A()) == "value");
+ REQUIRE(detect(B()) == "value");
+ REQUIRE(detect(C()) == "none");
+ REQUIRE(detect(AA()) == "value");
+}
+
+TEST_CASE("Meta vary", "[meta][vary]") {
+ std::variant<int, bool, TextView> v;
+ auto visitor = swoc::meta::vary{[](int &i) -> int { return i; }, [](bool &b) -> int { return b ? -1 : -2; },
+ [](TextView &tv) -> int { return swoc::svtou(tv); }};
+
+ v = 37;
+ REQUIRE(std::visit(visitor, v) == 37);
+ v = true;
+ REQUIRE(std::visit(visitor, v) == -1);
+ v = "956"_tv;
+ REQUIRE(std::visit(visitor, v) == 956);
+}
+
+TEST_CASE("Meta let", "[meta][let]") {
+ using swoc::meta::let;
+
+ unsigned x = 56;
+ {
+ REQUIRE(x == 56);
+ let guard(x, unsigned(3136));
+ REQUIRE(x == 3136);
+ // auto bogus = guard; // should not compile.
+ }
+ REQUIRE(x == 56);
+
+ // Checking move semantics - avoid reallocating the original string.
+ std::string s{"Evil Dave Rulz With An Iron Keyboard"}; // force allocation.
+ auto sptr = s.data();
+ {
+ char const *text = "Twas brillig and the slithy toves";
+ let guard(s, std::string(text));
+ REQUIRE(s == text);
+ REQUIRE(s.data() != sptr);
+ }
+ REQUIRE(s.data() == sptr);
+}
diff --git a/lib/swoc/unit_tests/test_range.cc b/lib/swoc/unit_tests/test_range.cc
new file mode 100644
index 0000000000..51e629d036
--- /dev/null
+++ b/lib/swoc/unit_tests/test_range.cc
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+/** @file
+ * Test Discrete Range.
+ */
+
+#include "swoc/DiscreteRange.h"
+#include "swoc/TextView.h"
+#include "catch.hpp"
+
+using swoc::TextView;
+using namespace std::literals;
+using namespace swoc::literals;
+
+using range_t = swoc::DiscreteRange<unsigned>;
+TEST_CASE("Discrete Range", "[libswoc][range]") {
+ range_t none; // empty range.
+ range_t single{56};
+ range_t r1{56, 100};
+ range_t r2{101, 200};
+ range_t r3{100, 200};
+
+ REQUIRE(single.contains(56));
+ REQUIRE_FALSE(single.contains(100));
+ REQUIRE(r1.is_adjacent_to(r2));
+ REQUIRE(r2.is_adjacent_to(r1));
+ REQUIRE(r1.is_left_adjacent_to(r2));
+ REQUIRE_FALSE(r2.is_left_adjacent_to(r1));
+
+ REQUIRE(r2.is_subset_of(r3));
+ REQUIRE(r3.is_superset_of(r2));
+
+ REQUIRE_FALSE(r3.is_subset_of(r2));
+ REQUIRE_FALSE(r2.is_superset_of(r3));
+
+ REQUIRE(r2.is_subset_of(r2));
+ REQUIRE_FALSE(r2.is_strict_subset_of(r2));
+ REQUIRE(r3.is_superset_of(r3));
+ REQUIRE_FALSE(r3.is_strict_superset_of(r3));
+}
diff --git a/lib/swoc/unit_tests/test_swoc_file.cc b/lib/swoc/unit_tests/test_swoc_file.cc
new file mode 100644
index 0000000000..0489023468
--- /dev/null
+++ b/lib/swoc/unit_tests/test_swoc_file.cc
@@ -0,0 +1,341 @@
+/** @file
+
+ swoc::file unit tests.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with the License. You may
+ obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ express or implied. See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#include <iostream>
+#include <unordered_map>
+#include <fstream>
+
+#include "swoc/swoc_file.h"
+#include "catch.hpp"
+
+using namespace swoc;
+using namespace swoc::literals;
+
+// --------------------
+
+static TextView
+set_env_var(TextView name, TextView value = ""_tv) {
+ TextView zret;
+ if (nullptr != getenv(name.data())) {
+ zret.assign(value);
+ }
+
+ if (!value.empty()) {
+ setenv(name.data(), value.data(), 1);
+ } else {
+ unsetenv(name.data());
+ }
+
+ return zret;
+}
+
+// --------------------
+TEST_CASE("swoc_file", "[libswoc][swoc_file]") {
+ file::path p1("/home");
+ REQUIRE(p1.string() == "/home");
+ auto p2 = p1 / "bob";
+ REQUIRE(p2.string() == "/home/bob");
+ p2 = p2 / "git/ats/";
+ REQUIRE(p2.string() == "/home/bob/git/ats/");
+ p2 /= "lib/ts";
+ REQUIRE(p2.string() == "/home/bob/git/ats/lib/ts");
+ p2 /= "/home/dave";
+ REQUIRE(p2.string() == "/home/dave");
+ auto p3 = file::path("/home/dave") / "git/tools";
+ REQUIRE(p3.string() == "/home/dave/git/tools");
+ REQUIRE(p3.parent_path().string() == "/home/dave/git");
+ REQUIRE(p3.parent_path().parent_path().string() == "/home/dave");
+ REQUIRE(p1.parent_path().string() == "/");
+
+ REQUIRE(p1 == p1);
+ REQUIRE(p1 != p2);
+
+ // This is primarily to check working with std::string and file::path.
+ std::string s1{"/home/evil/dave"};
+ file::path fp{s1};
+ std::error_code ec;
+ [[maybe_unused]] auto mtime = file::last_write_time(s1, ec);
+ REQUIRE(ec.value() != 0);
+
+ fp = s1; // Make sure this isn't ambiguous
+
+ // Verify path can be used as a hashed key for STL containers.
+ [[maybe_unused]] std::unordered_map<file::path, std::string> container;
+}
+
+/** Write a temporary file at a relative location.
+ * @return The name of the file.
+ */
+file::path
+write_a_temporary_file()
+{
+ constexpr std::string_view CONTENT = R"END(
+First line
+second line
+# Comment line
+
+
+Line after breaks.
+ dfa
+)END";
+ std::string temp_filename{"./test_swoc_file_tempXXXXXX"};
+ int fd = mkstemp(const_cast<char *>(temp_filename.data()));
+ REQUIRE(fd >= 0);
+ FILE * const open_file = fdopen(fd, "w");
+ REQUIRE(open_file != nullptr);
+ fputs(CONTENT.data(), open_file);
+ fclose(open_file);
+ close(fd);
+ return file::path{temp_filename};
+}
+
+TEST_CASE("swoc_file_io", "[libswoc][swoc_file_io]") {
+ auto file = write_a_temporary_file();
+ std::error_code ec;
+ auto content = swoc::file::load(file, ec);
+ REQUIRE(ec.value() == 0);
+ REQUIRE(content.size() > 0);
+ REQUIRE(content.find("second line") != content.npos);
+
+ // Check some file properties.
+ REQUIRE(swoc::file::is_readable(file) == true);
+ auto fs = swoc::file::status(file, ec);
+ REQUIRE(ec.value() == 0);
+ REQUIRE(swoc::file::is_dir(fs) == false);
+ REQUIRE(swoc::file::is_regular_file(fs) == true);
+
+ // See if converting to absolute works (at least somewhat).
+ REQUIRE(file.is_relative());
+ auto abs{swoc::file::absolute(file, ec)};
+ REQUIRE(ec.value() == 0);
+ REQUIRE(abs.is_absolute());
+ fs = swoc::file::status(abs, ec); // needs to be the same as for @a file
+ REQUIRE(ec.value() == 0);
+ REQUIRE(swoc::file::is_dir(fs) == false);
+ REQUIRE(swoc::file::is_regular_file(fs) == true);
+
+ // Clean up after ourselves.
+ remove(file, ec);
+
+ // Failure case.
+ file = "../unit-tests/no_such_file.txt";
+ content = swoc::file::load(file, ec);
+ REQUIRE(ec.value() == 2);
+ REQUIRE(swoc::file::is_readable(file) == false);
+
+ file::path f1{"/etc/passwd"};
+ file::path f2("/dev/null");
+ file::path f3("/argle/bargle");
+ REQUIRE(file::exists(f1));
+ REQUIRE(file::exists(f2));
+ REQUIRE_FALSE(file::exists(f3));
+ fs = file::status(f1, ec);
+ REQUIRE(file::exists(fs));
+ fs = file::status(f3, ec);
+ REQUIRE_FALSE(file::exists(fs));
+ REQUIRE_FALSE(file::exists(file::file_status{}));
+}
+
+TEST_CASE("path::filename", "[libswoc][file]") {
+ CHECK(file::path("/foo/bar.txt").filename() == file::path("bar.txt"));
+ CHECK(file::path("/foo/.bar").filename() == file::path(".bar"));
+ CHECK(file::path("/foo/bar").filename() == file::path("bar"));
+ CHECK(file::path("/foo/bar/").filename() == file::path(""));
+ CHECK(file::path("/foo/.").filename() == file::path("."));
+ CHECK(file::path("/foo/..").filename() == file::path(".."));
+ CHECK(file::path("/foo/../bar").filename() == file::path("bar"));
+ CHECK(file::path("/foo/../bar/").filename() == file::path(""));
+ CHECK(file::path(".").filename() == file::path("."));
+ CHECK(file::path("..").filename() == file::path(".."));
+ CHECK(file::path("/").filename() == file::path(""));
+ CHECK(file::path("//host").filename() == file::path("host"));
+
+ CHECK(file::path("/alpha/bravo").relative_path() == file::path("alpha/bravo"));
+ CHECK(file::path("alpha/bravo").relative_path() == file::path("alpha/bravo"));
+}
+
+TEST_CASE("swoc::file::temp_directory_path", "[libswoc][swoc_file]") {
+ // Clean all temp dir env variables and save the values.
+ std::string s1{set_env_var("TMPDIR")};
+ std::string s2{set_env_var("TEMPDIR")};
+ std::string s3{set_env_var("TMP")};
+ std::string s;
+
+ // If nothing defined return "/tmp"
+ CHECK(file::temp_directory_path() == file::path("/tmp"));
+
+ // TMPDIR defined.
+ set_env_var("TMPDIR", "/temp_alpha");
+ CHECK(file::temp_directory_path().view() == "/temp_alpha");
+ set_env_var("TMPDIR"); // clear
+
+ // TEMPDIR
+ set_env_var("TEMPDIR", "/temp_bravo");
+ CHECK(file::temp_directory_path().view() == "/temp_bravo");
+ // TMP defined, it should take precedence over TEMPDIR.
+ set_env_var("TMP", "/temp_alpha");
+ CHECK(file::temp_directory_path() == file::path("/temp_alpha"));
+ // TMPDIR defined, it should take precedence over TMP.
+ s = set_env_var("TMPDIR", "/temp_charlie");
+ CHECK(file::temp_directory_path() == file::path("/temp_charlie"));
+ set_env_var("TMPDIR", s);
+ set_env_var("TMP", s);
+ set_env_var("TEMPDIR", s);
+
+ // Restore all temp dir env variables to their previous state.
+ set_env_var("TMPDIR", s1);
+ set_env_var("TEMPDIR", s2);
+ set_env_var("TMP", s3);
+}
+
+TEST_CASE("file::path::create_directories", "[libswoc][swoc_file]") {
+ std::error_code ec;
+ file::path tempdir = file::temp_directory_path();
+
+ CHECK_FALSE(file::create_directory(file::path(), ec));
+ CHECK(ec.value() == EINVAL);
+ CHECK_FALSE(file::create_directories(file::path(), ec));
+
+ file::path testdir1 = tempdir / "dir1";
+ CHECK(file::create_directories(testdir1, ec));
+ CHECK(file::exists(testdir1));
+
+ file::path testdir2 = testdir1 / "dir2";
+ CHECK(file::create_directories(testdir2, ec));
+ CHECK(file::exists(testdir1));
+
+ // Cleanup
+ CHECK(file::remove_all(testdir1, ec) == 2);
+ CHECK_FALSE(file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::remove", "[libswoc][fs_file]") {
+ std::error_code ec;
+ file::path tempdir = file::temp_directory_path();
+
+ CHECK_FALSE(file::remove(file::path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ file::path testdir1 = tempdir / "dir1";
+ file::path testdir2 = testdir1 / "dir2";
+ file::path file1 = testdir2 / "alpha.txt";
+ file::path file2 = testdir2 / "bravo.txt";
+ file::path file3 = testdir2 / "charlie.txt";
+
+ // Simple creation and removal of a directory /tmp/dir1
+ CHECK(file::create_directories(testdir1, ec));
+ CHECK(file::exists(testdir1));
+ CHECK(file::remove(testdir1, ec));
+ CHECK_FALSE(file::exists(testdir1));
+
+ // Create /tmp/dir1/dir2 and remove /tmp/dir1/dir2 => /tmp/dir1 should exist
+ CHECK(file::create_directories(testdir2, ec));
+ CHECK(file::remove(testdir2, ec));
+ CHECK(file::exists(testdir1));
+
+ // Create a file, remove it, test if exists and then attempting to remove it again should fail.
+ CHECK(file::create_directories(testdir2, ec));
+ auto creatfile = [](char const *name) {
+ std::ofstream out(name);
+ out << "Simple test file " << name << std::endl;
+ out.close();
+ };
+ creatfile(file1.c_str());
+ creatfile(file2.c_str());
+ creatfile(file3.c_str());
+
+ CHECK(file::exists(file1));
+ CHECK(file::remove(file1, ec));
+ CHECK_FALSE(file::exists(file1));
+ CHECK_FALSE(file::remove(file1, ec));
+
+ // Clean up.
+ CHECK_FALSE(file::remove(testdir1, ec));
+ CHECK(file::remove_all(testdir1, ec) == 4);
+ CHECK_FALSE(file::exists(testdir1));
+}
+
+TEST_CASE("file::path::canonical", "[libswoc][swoc_file]") {
+ std::error_code ec;
+ file::path tempdir = file::canonical(file::temp_directory_path(), ec);
+ file::path testdir1 = tempdir / "libswoc_can_1";
+ file::path testdir2 = testdir1 / "libswoc_can_2";
+ file::path testdir3 = testdir2 / "libswoc_can_3";
+ file::path unorthodox = testdir3 / file::path("..") / file::path("..") / "libswoc_can_2";
+
+ // Invalid empty file::path.
+ CHECK(file::path() == file::canonical(file::path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ // Fail if directory does not exist
+ CHECK(file::path() == file::canonical(unorthodox, ec));
+ CHECK(ec.value() == ENOENT);
+
+ // Create the dir3 and test again
+ CHECK(create_directories(testdir3, ec));
+ CHECK(file::exists(testdir3));
+ CHECK(file::exists(testdir2));
+ CHECK(file::exists(testdir1));
+ CHECK(file::exists(unorthodox));
+ CHECK(file::canonical(unorthodox, ec) == testdir2);
+ CHECK(ec.value() == 0);
+
+ // Cleanup
+ CHECK(file::remove_all(testdir1, ec) > 0);
+ CHECK_FALSE(file::exists(testdir1));
+}
+
+TEST_CASE("file::path::copy", "[libts][swoc_file]") {
+ std::error_code ec;
+ file::path tempdir = file::temp_directory_path();
+ file::path testdir1 = tempdir / "libswoc_cp_alpha";
+ file::path testdir2 = testdir1 / "libswoc_cp_bravo";
+ file::path file1 = testdir2 / "original.txt";
+ file::path file2 = testdir2 / "copy.txt";
+
+ // Invalid empty path, both to and from parameters.
+ CHECK_FALSE(file::copy(file::path(), file::path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ CHECK(file::create_directories(testdir2, ec));
+ std::ofstream file(file1.string());
+ file << "Simple test file";
+ file.close();
+ CHECK(file::exists(file1));
+
+ // Invalid empty path, now from parameter is ok but to is empty
+ CHECK_FALSE(file::copy(file1, file::path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ // successful copy: "to" is directory
+ CHECK(file::copy(file1, testdir2, ec));
+ CHECK(ec.value() == 0);
+
+ // successful copy: "to" is file
+ CHECK(file::copy(file1, file2, ec));
+ CHECK(ec.value() == 0);
+
+ // Compare the content
+ CHECK(file::load(file1, ec) == file::load(file2, ec));
+
+ // Cleanup
+ CHECK(file::remove_all(testdir1, ec));
+ CHECK_FALSE(file::exists(testdir1));
+}
diff --git a/lib/swoc/unit_tests/unit_test_main.cc b/lib/swoc/unit_tests/unit_test_main.cc
new file mode 100644
index 0000000000..083564d4b0
--- /dev/null
+++ b/lib/swoc/unit_tests/unit_test_main.cc
@@ -0,0 +1,39 @@
+/** @file
+
+ This file used for catch based tests. It is the main() stub.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor
+ license agreements. See the NOTICE file distributed with this work for
+ additional information regarding copyright ownership. The ASF licenses this
+ file to you under the Apache License, Version 2.0 (the "License"); you may not
+ use this file except in compliance with the License. You may obtain a copy of
+ the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations under
+ the License.
+ */
+
+#define CATCH_CONFIG_RUNNER
+#include "catch.hpp"
+
+#include <array>
+
+extern void EX_BWF_Format_Init();
+extern void test_Errata_init();
+
+int
+main(int argc, char *argv[]) {
+ EX_BWF_Format_Init();
+ test_Errata_init();
+
+ int result = Catch::Session().run(argc, argv);
+
+ return result;
+}
diff --git a/lib/swoc/unit_tests/unit_tests.part b/lib/swoc/unit_tests/unit_tests.part
new file mode 100644
index 0000000000..a13f638045
--- /dev/null
+++ b/lib/swoc/unit_tests/unit_tests.part
@@ -0,0 +1,42 @@
+
+Import("*")
+PartName("tests")
+
+unit_test.DependsOn([
+ Component("libswoc.static", requires=REQ.DEFAULT(internal=False))
+ ])
+
+@unit_test.Group("tests")
+def run(env, test):
+
+ env.AppendUnique(
+ CCFLAGS=['-std=c++17'],
+ )
+
+ test.Sources = [
+ "unit_test_main.cc",
+
+ "test_BufferWriter.cc",
+ "test_bw_format.cc",
+ "test_Errata.cc",
+ "test_IntrusiveDList.cc",
+ "test_IntrusiveHashMap.cc",
+ "test_ip.cc",
+ "test_Lexicon.cc",
+ "test_MemSpan.cc",
+ "test_MemArena.cc",
+ "test_meta.cc",
+ "test_TextView.cc",
+ "test_Scalar.cc",
+ "test_swoc_file.cc",
+ "ex_bw_format.cc",
+ "ex_IntrusiveDList.cc",
+ "ex_MemArena.cc",
+ "ex_TextView.cc",
+ ]
+
+ # tests are defined to have a tree structure for the files
+ test.DataFiles=[
+ Pattern(src_dir="#",includes=['doc/conf.py','unit_tests/examples/resolver.txt', 'unit_tests/test_swoc_file.cc'])
+ ]
+
diff --git a/src/proxy/http/HttpSessionManager.cc b/src/proxy/http/HttpSessionManager.cc
index 9bb015053e..b64ba0ce70 100644
--- a/src/proxy/http/HttpSessionManager.cc
+++ b/src/proxy/http/HttpSessionManager.cc
@@ -34,6 +34,7 @@
#include "proxy/ProxySession.h"
#include "proxy/http/HttpSM.h"
#include "proxy/http/HttpDebugNames.h"
+#include <iterator>
// Initialize a thread to handle HTTP session management
void
@@ -151,47 +152,52 @@ ServerSessionPool::acquireSession(sockaddr const *addr, CryptoHash const &hostna
// This is broken out because only in this case do we check the host hash first. The range must be checked
// to verify an upstream that matches port and SNI name is selected. Walk backwards to select oldest.
in_port_t port = ats_ip_port_cast(addr);
- auto first = m_fqdn_pool.find(hostname_hash);
- while (first != m_fqdn_pool.end() && first->hostname_hash == hostname_hash) {
- Debug("http_ss", "Compare port 0x%x against 0x%x", port, ats_ip_port_cast(first->get_remote_addr()));
- if (port == ats_ip_port_cast(first->get_remote_addr()) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_SNI) || validate_sni(sm, first->get_netvc())) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTSNISYNC) || validate_host_sni(sm, first->get_netvc())) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_CERT) || validate_cert(sm, first->get_netvc()))) {
+ auto range = m_fqdn_pool.equal_range(hostname_hash);
+ auto iter = std::make_reverse_iterator(range.end());
+ auto const end = std::make_reverse_iterator(range.begin());
+ while (iter != end) {
+ Debug("http_ss", "Compare port 0x%x against 0x%x", port, ats_ip_port_cast(iter->get_remote_addr()));
+ if (port == ats_ip_port_cast(iter->get_remote_addr()) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_SNI) || validate_sni(sm, iter->get_netvc())) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTSNISYNC) || validate_host_sni(sm, iter->get_netvc())) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_CERT) || validate_cert(sm, iter->get_netvc()))) {
zret = HSM_DONE;
break;
}
- ++first;
+ ++iter;
}
if (zret == HSM_DONE) {
- to_return = first;
+ to_return = &*iter;
if (!to_return->is_multiplexing()) {
this->removeSession(to_return);
}
- } else if (first != m_fqdn_pool.end()) {
+ } else if (iter != end) {
Debug("http_ss", "Failed find entry due to name mismatch %s", sm->t_state.current.server->name);
}
} else if (TS_SERVER_SESSION_SHARING_MATCH_MASK_IP & match_style) { // matching is not disabled.
- auto first = m_ip_pool.find(addr);
+ auto range = m_ip_pool.equal_range(addr);
+ // We want to access the sessions in LIFO order, so start from the back of the list.
+ auto iter = std::make_reverse_iterator(range.end());
+ auto const end = std::make_reverse_iterator(range.begin());
// The range is all that is needed in the match IP case, otherwise need to scan for matching fqdn
// And matches the other constraints as well
// Note the port is matched as part of the address key so it doesn't need to be checked again.
if (match_style & (~TS_SERVER_SESSION_SHARING_MATCH_MASK_IP)) {
- while (first != m_ip_pool.end() && ats_ip_addr_port_eq(first->get_remote_addr(), addr)) {
- if ((!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTONLY) || first->hostname_hash == hostname_hash) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_SNI) || validate_sni(sm, first->get_netvc())) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTSNISYNC) || validate_host_sni(sm, first->get_netvc())) &&
- (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_CERT) || validate_cert(sm, first->get_netvc()))) {
+ while (iter != end) {
+ if ((!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTONLY) || iter->hostname_hash == hostname_hash) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_SNI) || validate_sni(sm, iter->get_netvc())) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_HOSTSNISYNC) || validate_host_sni(sm, iter->get_netvc())) &&
+ (!(match_style & TS_SERVER_SESSION_SHARING_MATCH_MASK_CERT) || validate_cert(sm, iter->get_netvc()))) {
zret = HSM_DONE;
break;
}
- ++first;
+ ++iter;
}
- } else if (first != m_ip_pool.end()) {
+ } else if (iter != end) {
zret = HSM_DONE;
}
if (zret == HSM_DONE) {
- to_return = first;
+ to_return = &*iter;
if (!to_return->is_multiplexing()) {
this->removeSession(to_return);
}