diff --git a/lib/swoc/CMakeLists.txt b/lib/swoc/CMakeLists.txt index 05eef1bed4f..5d877aeb63a 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 201f093e71b..4bbe3b63615 100644 --- a/lib/swoc/include/swoc/IntrusiveHashMap.h +++ b/lib/swoc/include/swoc/IntrusiveHashMap.h @@ -514,6 +514,15 @@ IntrusiveHashMap::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 01ddb659cd1..9e914c8a630 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 00000000000..bd2fe052696 --- /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 00000000000..3c59a9ed47b --- /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 +#include +#include +#include + +#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; + +public: + ~Container(); + + template 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 +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(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(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(t->_next); + } + static self_type *& + prev_ptr(self_type *t) { + return swoc::ptr_ref_cast(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; + friend Linkage; + + std::string const & + payload() const { + return _payload; + } +}; + +TEST_CASE("IntrusiveDList Inheritance", "[libswoc][IntrusiveDList][example]") { + IntrusiveDList 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 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 00000000000..1be975e9ce8 --- /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 + +#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 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; + +TEST_CASE("Lexicon Example", "[libts][Lexicon]") { + swoc::IPSpace 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(idx)); // set the bit + } + } + space.mark(r, flags); // store the flags in the spae. + } + } + // doc.load.end + + using AddrCase = std::tuple; + using swoc::IPAddr; + std::array 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 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 const Example2{ + {{NetType::EXTERNAL, "external"}, {NetType::PROD, "prod"}, {NetType::SECURE, "secure"}, {NetType::EDGE, "edge"}}, +}; +// doc.ctor.2.end + +// doc.ctor.3.begin +swoc::Lexicon 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 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 00000000000..a6f7098e573 --- /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 +#include +#include +#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(); + memcpy(span, view); + return span; +} + +template 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(std::move(tmp)); + arena->~MemArena(); + } + + { + std::unique_ptr 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 arena(ta.make(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> arena(ta.make(std::move(ta))); + } + + { + MemArena ta; + // 16 bytes + std::unique_ptr arena(ta.make(std::move(ta)), &Destroy); + } + + { + MemArena ta; + // 16 bytes + std::unique_ptr arena(ta.make(std::move(ta)), + [](MemArena *arena) -> void { arena->~MemArena(); }); + } + + { + auto destroyer = [](MemArena *arena) -> void { arena->~MemArena(); }; + MemArena ta; + // 8 bytes + std::unique_ptr arena(ta.make(std::move(ta)), destroyer); + } +}; + +template +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(); +} + +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 { + 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(); + REQUIRE(thing->name == "name"); + REQUIRE(thing->n == 10); + + thing = arena.make(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(); // 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; + Map *ihm = arena.make(); + + { + std::string key_1{"Key One"}; + std::string key_2{"Key Two"}; + + ihm->insert(arena.make(localize(arena, key_1), 1)); + ihm->insert(arena.make(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 00000000000..09317521bee --- /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 +#include +#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 alphabet{ + {"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"} +}; + +// -- doc csv start +void +parse_csv(TextView src, std::function 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 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 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 +#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 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 00000000000..b1461961314 --- /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 +#include + +#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; ///< 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 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 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::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 00000000000..4291bd6fa35 --- /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 +#include +#include + +#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(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 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; + + 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 { + using super_type = swoc::bwf::ContextNames; + + 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; + 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(*std::any_cast(value)); + else if (typeid(unsigned *) == value.type()) + v = *std::any_cast(value); + else if (typeid(size_t *) == value.type()) + v = static_cast(*std::any_cast(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 +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(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 00000000000..8db96fefc10 --- /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 +#include +#include + +#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; + // Example data type. + using Data = std::tuple; + + // 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 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 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 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{ranges_1.data(), ranges_1.size()}); + dump(space); + + // test ranges 2 + std::array 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{ranges_2.data(), ranges_2.size()}); + dump(space); + + // test ranges 3 + std::array ranges_3 = {{{"100.0.2.0-100.0.4.255", make_bits({2, 3, 29})}}}; + + marker(space, MemSpan{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 const &rhs) -> bool { + for (auto idx : rhs) + lhs[idx] = true; + return true; + }; + + std::initializer_list 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; + + /** 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 span) = 0; + + protected: + friend class Table; + + TextView _name; ///< Name of the property. + unsigned _idx = std::numeric_limits::max(); ///< Column index. + size_t _offset = std::numeric_limits::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 P *add_column(std::unique_ptr

&&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 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 span_for(Property const &prop) const; + + protected: + MemSpan _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 _columns; + + /// IPSpace type. + using space = IPSpace; + 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 +P * +Table::add_column(std::unique_ptr

&&col) { + auto prop = col.get(); + auto idx = _columns.size(); + col->assign_offset(_size); + col->assign_idx(idx); + _size += static_cast(prop)->size(); + _columns.emplace_back(std::move(col)); + return prop; +} + +TextView +Table::localize(TextView const &src) { + auto span = _arena.alloc(src.size()).rebind(); + 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(); // 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 +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 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 span) override; + /// List of tags. + std::vector _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 _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 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 span) override; + bool + needs_localized_token() const override { + return true; + } +}; + +// --- +bool +StringProperty::parse(TextView token, MemSpan span) { + memcpy(span.data(), &token, sizeof(token)); + return true; +} + +FlagGroupProperty::FlagGroupProperty(TextView const &name, std::initializer_list tags) : super_type(name) { + _tags.reserve(tags.size()); + for (auto const &tag : tags) { + _tags.emplace_back(tag); + } +} + +bool +FlagGroupProperty::parse(TextView token, MemSpan 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 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()[0] = spot - _tags.begin(); + return true; +} + +TextView +EnumProperty::operator()(Table::Row const &row) const { + auto idx = row.span_for(*this).rebind()[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("owner")); + auto colo = table.add_column(std::make_unique("colo")); + auto flags = table.add_column(std::make_unique("flags"_tv, flag_names)); + [[maybe_unused]] auto description = table.add_column(std::make_unique("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 00000000000..75b969d5b0f --- /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 00000000000..9eeee19ae45 --- /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 +#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(x).write(three[0]).write(three[1]).write(three[2]); + + REQUIRE(x.good); +} + +namespace { +template using LBW = swoc::LocalBufferWriter; +} + +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 +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(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()}; + 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 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()); + 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 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(); + 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(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 00000000000..ea4e2e8a81d --- /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 +#include +#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 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 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(Severity_Names.data(), Severity_Names.size()); + + swoc::Errata::register_sink([](swoc::Errata const &errata) -> void { bwprint(ErrataSinkText, "{}", errata); }); +} + +Errata +Noteworthy(std::string_view text) { + return Errata{ERRATA_INFO, text}; +} + +Errata +cycle(Errata &erratum) { + return std::move(erratum.note("Note well, young one!")); +} + +TEST_CASE("Errata copy", "[libswoc][Errata]") { + auto notes = Noteworthy("Evil Dave Rulz."); + REQUIRE(notes.length() == 1); + REQUIRE(notes.begin()->text() == "Evil Dave Rulz."); + + notes = cycle(notes); + REQUIRE(notes.length() == 2); + + Errata erratum; + REQUIRE(erratum.length() == 0); + erratum.note("Diagnostics"); + REQUIRE(erratum.length() == 1); + erratum.note("Information"); + REQUIRE(erratum.length() == 2); + + // Test internal allocation boundaries. + notes.clear(); + std::string_view text{"0123456789012345678901234567890123456789"}; + for (int i = 0; i < 50; ++i) { + notes.note(text); + } + REQUIRE(notes.length() == 50); + REQUIRE(notes.begin()->text() == text); + bool match_p = true; + for (auto &¬e : notes) { + if (note.text() != text) { + match_p = false; + break; + } + } + REQUIRE(match_p); +}; + +TEST_CASE("Rv", "[libswoc][Errata]") { + Rv zret; + struct Thing { + char const *s = "thing"; + }; + using ThingHandle = std::unique_ptr; + + 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 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{56, Errata(ERRATA_DIAG, "Test rvalue diag")}); + test(ERRATA_INFO, Rv{56, Errata(ERRATA_INFO, "Test rvalue info")}); + test(ERRATA_WARN, Rv{56, Errata(ERRATA_WARN, "Test rvalue warn")}); + test(ERRATA_ERROR, Rv{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 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 handle = std::make_unique(); + handle->s = "made"; + return {std::move(handle)}; + }; + + auto &&[tr2, te2]{maker()}; + REQUIRE(tr2->s == "made"sv); +}; + +// DOC -> NoteInfo +template +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 without change at the call site. + int size = -7; + auto f = [&]() -> Rv { + 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 +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...)), swoc::bwf::Errno(err)); +} + +template +Errata +errata_errno(Errata::Severity s, swoc::TextView fmt, Args &&...args) { + return errata_errno(errno, s, fmt, std::forward(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 00000000000..51f57a09e89 --- /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 +#include +#include +#include + +#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; + +} // 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(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 _links; + }; + + using S_List = swoc::IntrusiveDList>; + [[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 00000000000..aa228ba8e0e --- /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 +#include +#include +#include +#include +#include + +#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 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; + +} // 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 strings; + + std::uniform_int_distribution char_gen{'a', 'z'}; + std::uniform_int_distribution 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(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 00000000000..a9538e0bc0b --- /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 + +#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; + +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(0xBADD00D)] == "INVALID"); + REQUIRE(exnames[exnames[static_cast(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; + 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; +using HexLexicon = swoc::Lexicon; + +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; + 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 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::max(); + [[maybe_unused]] swoc::Lexicon _locations1{INVALID_LOCATION}; + [[maybe_unused]] swoc::Lexicon _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 00000000000..e98a982cc35 --- /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 +#include +#include +#include +#include +#include + +#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 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(); + 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(); + 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(); + 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()}; + + REQUIRE(thing_one->ten == 10); + REQUIRE(thing_one->name == "name"); + + thing_one = arena.make(17, "bob"sv); + + REQUIRE(thing_one->name == "bob"); + REQUIRE(thing_one->ten == 17); + + thing_one = arena.make("Dave", 137); + + REQUIRE(thing_one->name == "Dave"); + REQUIRE(thing_one->ten == 137); + + thing_one = arena.make(9999); + + REQUIRE(thing_one->ten == 9999); + REQUIRE(thing_one->name == "name"); + + thing_one = arena.make("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(); + swoc::MemSpan s2 = arena.alloc(16).rebind(); + swoc::MemSpan s3 = arena.alloc(16).rebind(); + + 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()}; + MemSpan m2{arena.alloc(32000).rebind()}; + MemSpan m3{arena.alloc(64000).rebind()}; + + 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 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(); + 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 arena(MemArena::construct_self_contained(), + [](MemArena *arena) -> void { arena->~MemArena(); }); + static constexpr unsigned MAX = 512; + std::uniform_int_distribution 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 arena(nullptr, MemArena::destroyer); + arena.reset(MemArena::construct_self_contained()); + static constexpr unsigned MAX = 512; + std::uniform_int_distribution 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 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 arena{nullptr, std::destroy_at}; + } thing; + thing.x = 56; // force access to instance. + } +} + +// --- temporary allocation +TEST_CASE("MemArena temporary", "[libswoc][MemArena][tmp]") { + MemArena arena; + std::vector strings; + + static constexpr short MAX{8000}; + static constexpr int N{100}; + + std::uniform_int_distribution length_gen{100, MAX}; + std::array 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(); + 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 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 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 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() && ( !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; + 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; + 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(&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; + MemArena arena; + REQUIRE(arena.size() == 0); + C *c = arena.make(&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(&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 00000000000..63555304da9 --- /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 +#include + +#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 span(buff, sizeof(buff)); + MemSpan 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 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(); + 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(); // 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(); + REQUIRE_THROWS_AS(span.rebind(), std::invalid_argument); + REQUIRE_THROWS_AS(vs.rebind(), std::invalid_argument); + vs.rebind(); // check for void -> void rebind compiling. + REQUIRE_FALSE(std::is_const_v); + + // 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); + auto vcs = vs.rebind(); + REQUIRE(vcs.size() == 1022); + REQUIRE(std::is_const_v); + MemSpan char_cv{buff, 64}; + auto void_cv = char_cv.rebind(); + REQUIRE(std::is_const_v); + + // Check for assignment to void. + vs = span; + REQUIRE(vs.size() == 1022); + + // Test C array constructors. + MemSpan a{buff}; + REQUIRE(a.size() == sizeof(buff)); + REQUIRE(a.data() == buff); + float floats[] = {1.1, 2.2, 3.3, 4.4, 5.5}; + MemSpan fspan{floats}; + REQUIRE(fspan.size() == 5); + REQUIRE(fspan[3] == 4.4f); + MemSpan 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 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))); + + // By design, MemSpan won't construct from a literal string because it's const. + // MemSpan s1{"Evil Dave Rulz"}; // Should not compile. + + uint8_t bytes[]{5, 4, 3, 2, 1, 0}; + MemSpan s2{bytes}; + REQUIRE(s2.size() == sizeof(bytes)); // terminal nul is in view + } + + SECTION("void const") { + 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 + } +} + +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()}; + + 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", "[libswoc][MemSpan]") { + TextView tv = "bike shed"; + char buff[1024]; + + MemSpan span(buff, sizeof(buff)); + MemSpan cspan(span); + MemSpan ccspan(tv.data(), tv.size()); + CHECK_FALSE(cspan.is_same(ccspan)); + ccspan = span; + + // auto bad_span = ccspan.rebind(); // 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 a(buff, sizeof(buff)); + MemSpan b; + b = a.align(); + REQUIRE(b.data() == a.data()); + REQUIRE(b.size() == a.size()); + + b = a.suffix(a.size() - 2).align(); + REQUIRE(b.data() != a.data()); + REQUIRE(b.size() != a.size()); + auto i = a.rebind(); + REQUIRE(b.data() == i.data() + 1); + + b = a.suffix(a.size() - 2).align(alignof(int)); + REQUIRE(b.data() == i.data() + 1); + REQUIRE(b.rebind().size() == i.size() - 1); +}; + +TEST_CASE("MemSpan conversions", "[libswoc][MemSpan]") { + std::array a1; + std::string_view sv{"Evil Dave"}; + swoc::TextView tv{sv}; + std::string str{sv}; + auto const &ra1 = a1; + auto ms1 = MemSpan(a1); // construct from array + auto ms2 = MemSpan(a1); // construct from array, deduction guide + REQUIRE(ms2.size() == a1.size()); + auto ms3 = MemSpan(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 ms5{ra1}; + REQUIRE(ms5.size() == ra1.size()); + // Construct a span of constant from a ref to an array with non-const type. + MemSpan ms6{a1}; + + MemSpan va1{a1}; + REQUIRE(va1.size() == a1.size() * sizeof(*(a1.data()))); + MemSpan cva1{a1}; + REQUIRE(cva1.size() == a1.size() * sizeof(*(a1.data()))); + + [[maybe_unused]] MemSpan c1 = ms1; // Conversion from T to T const. + + MemSpan c2{sv.data(), sv.size()}; + [[maybe_unused]] MemSpan vc2{c2}; + // Generic construction from STL containers. + MemSpan c3{sv}; + [[maybe_unused]] MemSpan c7{str}; + [[maybe_unused]] MemSpan c4{str}; + auto const &cstr{str}; + MemSpan c8{cstr}; + REQUIRE(c8.size() == cstr.size()); + // [[maybe_unused]] MemSpan c9{cstr}; // should not compile, const container to non-const span. + + [[maybe_unused]] MemSpan c5{str}; + [[maybe_unused]] MemSpan c6{sv}; + + [[maybe_unused]] MemSpan c10{sv}; + [[maybe_unused]] MemSpan c11{tv}; + + char const *args[] = {"alpha", "bravo", "charlie", "delta"}; + MemSpan span_args{args}; + MemSpan 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(); + MemSpan raw = span; + REQUIRE(raw.size() == sizeof(Thing)); + MemSpan craw = raw; + REQUIRE(raw.size() == craw.size()); + craw = span; + REQUIRE(raw.size() == craw.size()); + + REQUIRE(raw.rebind().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 00000000000..f7f4479e8ff --- /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; + +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; + + PageSize pg1(1); + REQUIRE(pg1.count() == 1); + REQUIRE(pg1.value() == SCALE); + + using Size_1 = swoc::Scalar; + using Size_2 = swoc::Scalar; + + 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; + using Size_2 = swoc::Scalar; + + 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 00000000000..ebeb6c23a79 --- /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 +#include +#include +#include +#include +#include + +#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::max(); + static constexpr auto IMAX = std::numeric_limits::max(); + static constexpr auto IMIN = std::numeric_limits::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::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 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 map; + std::unordered_map 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 00000000000..ebe7d34b0d3 --- /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 +#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 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 v1; + v1.emplace_back(count); + } + REQUIRE(count == 1); + + count = 0; + { // force use of dynamic memory. + Vectray 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 00000000000..4f98abc66e7 --- /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 +#include +#include + +#include + +#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(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(ptr); + bw.clear(); + bw.print("{}", int_ptr); + REQUIRE(bw.view() == "0xbadd0956"); + char char_ptr[] = "delain"; + bw.clear(); + bw.print("{:x}", static_cast(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 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())); + } +} + +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 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::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::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(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(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(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(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 00000000000..bb7eadfc31a --- /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 +#include +#include + +#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 +void +dump(IPSpace

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 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 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 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; + 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, 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, 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, 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 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; + + std::array>, 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; + // 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 idx) -> PAYLOAD { + PAYLOAD bits; + for (auto bit : idx) { + bits[bit] = true; + } + return bits; + }; + + std::array, 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 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; + 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::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::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); + auto &v1 = *spot; + auto &p1 = get<1>(v1); + + if (auto &&[r, p] = *(cspace.find(a1)); !r.empty()) { + static_assert(std::is_same_v); + 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 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 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; // rack, pod, code + using Src_2 = std::tuple; // 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 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, 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; + + std::array, 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; + + std::array, 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 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 00000000000..d0d6ec59854 --- /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 +#include + +#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::value); + REQUIRE(false == swoc::meta::is_any_of::value); + REQUIRE(true == swoc::meta::is_any_of::value); + REQUIRE(false == swoc::meta::is_any_of::value); + REQUIRE(false == swoc::meta::is_any_of::value); // verify degenerate use case. + + REQUIRE(true == swoc::meta::is_any_of_v); + REQUIRE(false == swoc::meta::is_any_of_v); +} + +// Start of ts::meta testing. + +namespace { +template +auto +detect(T &&t, swoc::meta::CaseTag<0>) -> std::string_view { + return "none"; +} +template +auto +detect(T &&t, swoc::meta::CaseTag<1>) -> decltype(t._value, std::string_view()) { + return "value"; +} +template +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 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 00000000000..51e629d0361 --- /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; +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 00000000000..0489023468f --- /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 +#include +#include + +#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 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(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 00000000000..083564d4b05 --- /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 + +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 00000000000..a13f638045b --- /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 9bb015053e7..b64ba0ce702 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 // 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); }