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 &&note : 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);
       }