flush [ci skip]
authorMichael Wallner <mike@php.net>
Wed, 2 Sep 2020 16:38:28 +0000 (18:38 +0200)
committerMichael Wallner <mike@php.net>
Wed, 2 Sep 2020 16:38:57 +0000 (18:38 +0200)
33 files changed:
include/libhashkit-1.0/hashkit.hpp
testing/CMakeLists.txt
testing/hashkit/basic.cpp [new file with mode: 0644]
testing/lib.cpp [new file with mode: 0644]
testing/lib/Cluster.cpp
testing/lib/Cluster.hpp
testing/lib/Connection.cpp [new file with mode: 0644]
testing/lib/Connection.hpp [new file with mode: 0644]
testing/lib/ForkAndExec.cpp [new file with mode: 0644]
testing/lib/ForkAndExec.hpp [new file with mode: 0644]
testing/lib/MemcachedCluster.cpp [new file with mode: 0644]
testing/lib/MemcachedCluster.hpp [new file with mode: 0644]
testing/lib/Poll.cpp [deleted file]
testing/lib/Poll.hpp [deleted file]
testing/lib/Retry.cpp [new file with mode: 0644]
testing/lib/Retry.hpp [new file with mode: 0644]
testing/lib/Server.cpp
testing/lib/Server.hpp
testing/lib/WaitForConn.cpp [deleted file]
testing/lib/WaitForConn.hpp [deleted file]
testing/lib/WaitForExec.cpp [deleted file]
testing/lib/WaitForExec.hpp [deleted file]
testing/lib/common.cpp [new file with mode: 0644]
testing/lib/common.hpp [new file with mode: 0644]
testing/lib/random_.cpp [deleted file]
testing/lib/random_.hpp [deleted file]
testing/memcached/basic.cpp [new file with mode: 0644]
testing/memcached/callbacks.cpp [new file with mode: 0644]
testing/memcached/dump.cpp [new file with mode: 0644]
testing/memcached/encoding_key.cpp [new file with mode: 0644]
testing/memcached/exist.cpp [new file with mode: 0644]
testing/memcached/servers.cpp [new file with mode: 0644]
testing/server.cpp [deleted file]

index a953d72df4c711d91d2e5809f90fc0fbb7922559..11a250fa3870d1d313d847a83d86f8c2018118f1 100644 (file)
@@ -85,7 +85,7 @@ public:
 
   hashkit_return_t set_distribution_function(hashkit_hash_algorithm_t hash_algorithm)
   {
-    return hashkit_set_function(&self, hash_algorithm);
+    return hashkit_set_distribution_function(&self, hash_algorithm);
   }
 
   ~Hashkit()
index 4bfcff8125c2fbb47c7a8df6a20d600c2b533b04..ab2f30e8adfa68880e7a86ff26d8952c3b1eea5b 100644 (file)
@@ -2,16 +2,26 @@
 add_executable(catch_main
         main.cpp
 
-        lib/random_.cpp
+        lib/common.cpp
 
         lib/Cluster.cpp
-        lib/Poll.cpp
+        lib/Connection.cpp
+        lib/ForkAndExec.cpp
+        lib/MemcachedCluster.cpp
         lib/Server.cpp
-        lib/WaitForConn.cpp
-        lib/WaitForExec.cpp
+        lib/Retry.cpp
 
-        server.cpp
+        lib.cpp
+        hashkit/basic.cpp
+        memcached/basic.cpp
+        memcached/callbacks.cpp
+        memcached/servers.cpp
+        memcached/dump.cpp
+        memcached/encoding_key.cpp
+        memcached/exist.cpp
         )
 
 set_target_properties(catch_main PROPERTIES
         CXX_STANDARD 17)
+
+target_link_libraries(catch_main libhashkit libmemcached)
diff --git a/testing/hashkit/basic.cpp b/testing/hashkit/basic.cpp
new file mode 100644 (file)
index 0000000..b162b9c
--- /dev/null
@@ -0,0 +1,217 @@
+#include "../lib/common.hpp"
+
+#include "libhashkit-1.0/hashkit.hpp"
+
+static const char *input[] = {
+    "apple",
+    "beat",
+    "carrot",
+    "daikon",
+    "eggplant",
+    "flower",
+    "green",
+    "hide",
+    "ick",
+    "jack",
+    "kick",
+    "lime",
+    "mushrooms",
+    "nectarine",
+    "orange",
+    "peach",
+    "quant",
+    "ripen",
+    "strawberry",
+    "tang",
+    "up",
+    "volumne",
+    "when",
+    "yellow",
+    "zip",
+};
+
+static const uint32_t output[][sizeof(input)/sizeof(*input)] = {
+          // one_at_a_time
+          { 2297466611U, 3902465932U, 469785835U, 1937308741U,
+          261917617U, 3785641677U, 1439605128U, 1649152283U,
+          1493851484U, 1246520657U, 2221159044U, 1973511823U,
+          384136800U, 214358653U, 2379473940U, 4269788650U,
+          2864377005U, 2638630052U, 427683330U, 990491717U,
+          1747111141U, 792127364U, 2599214128U, 2553037199U,
+          2509838425U },
+
+          // md5
+          { 3195025439U, 2556848621U, 3724893440U, 3332385401U,
+          245758794U, 2550894432U, 121710495U, 3053817768U,
+          1250994555U, 1862072655U, 2631955953U, 2951528551U,
+          1451250070U, 2820856945U, 2060845566U, 3646985608U,
+          2138080750U, 217675895U, 2230934345U, 1234361223U,
+          3968582726U, 2455685270U, 1293568479U, 199067604U,
+          2042482093U },
+
+          // crc
+          { 10542U, 22009U, 14526U, 19510U, 19432U, 10199U, 20634U,
+          9369U, 11511U, 10362U, 7893U, 31289U, 11313U, 9354U,
+          7621U, 30628U, 15218U, 25967U, 2695U, 9380U,
+          17300U, 28156U, 9192U, 20484U, 16925U },
+
+          // fnv1_64
+          { 473199127U, 4148981457U, 3971873300U, 3257986707U,
+          1722477987U, 2991193800U, 4147007314U, 3633179701U,
+          1805162104U, 3503289120U, 3395702895U, 3325073042U,
+          2345265314U, 3340346032U, 2722964135U, 1173398992U,
+          2815549194U, 2562818319U, 224996066U, 2680194749U,
+          3035305390U, 246890365U, 2395624193U, 4145193337U,
+          1801941682U },
+
+          // fnv1a_64
+          { 1488911807U, 2500855813U, 1510099634U, 1390325195U,
+          3647689787U, 3241528582U, 1669328060U, 2604311949U,
+          734810122U, 1516407546U, 560948863U, 1767346780U,
+          561034892U, 4156330026U, 3716417003U, 3475297030U,
+          1518272172U, 227211583U, 3938128828U, 126112909U,
+          3043416448U, 3131561933U, 1328739897U, 2455664041U,
+          2272238452U },
+
+          // fnv1_32
+          { 67176023U, 1190179409U, 2043204404U, 3221866419U,
+          2567703427U, 3787535528U, 4147287986U, 3500475733U,
+          344481048U, 3865235296U, 2181839183U, 119581266U,
+          510234242U, 4248244304U, 1362796839U, 103389328U,
+          1449620010U, 182962511U, 3554262370U, 3206747549U,
+          1551306158U, 4127558461U, 1889140833U, 2774173721U,
+          1180552018U },
+
+          // fnv1a_32
+          { 280767167U, 2421315013U, 3072375666U, 855001899U,
+          459261019U, 3521085446U, 18738364U, 1625305005U,
+          2162232970U, 777243802U, 3323728671U, 132336572U,
+          3654473228U, 260679466U, 1169454059U, 2698319462U,
+          1062177260U, 235516991U, 2218399068U, 405302637U,
+          1128467232U, 3579622413U, 2138539289U, 96429129U,
+          2877453236U },
+
+          // hsieh
+#ifdef HAVE_HSIEH_HASH
+          { 3738850110U, 3636226060U, 3821074029U, 3489929160U, 3485772682U, 80540287U,
+          1805464076U, 1895033657U, 409795758U, 979934958U, 3634096985U, 1284445480U,
+          2265380744U, 707972988U, 353823508U, 1549198350U, 1327930172U, 9304163U,
+          4220749037U, 2493964934U, 2777873870U, 2057831732U, 1510213931U, 2027828987U,
+          3395453351U },
+#else
+          {  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
+#endif
+
+          // murmur
+#ifdef HAVE_MURMUR_HASH
+          // murmur
+          { 4142305122U, 734504955U, 3802834688U, 4076891445U,
+          387802650U, 560515427U, 3274673488U, 3150339524U,
+          1527441970U, 2728642900U, 3613992239U, 2938419259U,
+          2321988328U, 1145154116U, 4038540960U, 2224541613U,
+          264013145U, 3995512858U, 2400956718U, 2346666219U,
+          926327338U, 442757446U, 1770805201U, 560483147U,
+          3902279934U },
+          // murmur3
+          { 1120212521U, 1448785489U, 4186307405U, 2686268514U,
+          444808887U, 221750260U, 3074673162U, 1946933257U,
+          2826416675U, 2430719166U, 3200429559U, 297894347U,
+          732888124U, 4050076964U, 3298336176U, 1336207361U,
+          810553576U, 3748182674U, 3860119212U, 3439537197U,
+          3044240981U, 1464271804U, 3896193724U, 2915115798U,
+          1702843840U },
+#else
+          { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
+          { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
+#endif
+
+          // jenkins
+          { 1442444624U, 4253821186U, 1885058256U, 2120131735U,
+          3261968576U, 3515188778U, 4232909173U, 4288625128U,
+          1812047395U, 3689182164U, 2502979932U, 1214050606U,
+          2415988847U, 1494268927U, 1025545760U, 3920481083U,
+          4153263658U, 3824871822U, 3072759809U, 798622255U,
+          3065432577U, 1453328165U, 2691550971U, 3408888387U,
+          2629893356U }
+};
+
+TEST_CASE("hashkit") {
+  hashkit_st st, *hp = hashkit_create(nullptr);
+  Hashkit stack;
+  Hashkit *heap = new Hashkit;
+
+  REQUIRE(hashkit_create(&st));
+  REQUIRE(hp);
+
+  SECTION("can copy") {
+    Hashkit stack_copy(stack);
+    Hashkit *heap_copy(heap);
+    hashkit_st st_copy, *st_ptr;
+
+    (void) stack_copy;
+    (void) heap_copy;
+
+    st_ptr = hashkit_clone(&st_copy, &st);
+    REQUIRE(st_ptr == &st_copy);
+    REQUIRE(hashkit_compare(st_ptr, &st_copy));
+
+    SUCCEED("OK");
+  }
+
+  SECTION("can assign") {
+    Hashkit stack_copy;
+
+    stack_copy = stack;
+    (void) stack_copy;
+
+    SUCCEED("OK");
+  }
+
+  SECTION("can digest default") {
+    REQUIRE(2297466611U == stack.digest(LITERAL("apple")));
+    REQUIRE(2297466611U == hashkit_digest(&st, LITERAL("apple")));
+  }
+
+  SECTION("can set hash function") {
+    for (int f = HASHKIT_HASH_DEFAULT; f < HASHKIT_HASH_MAX; ++f) {
+      auto h = static_cast<hashkit_hash_algorithm_t>(f);
+
+      if (h == HASHKIT_HASH_CUSTOM) {
+        continue;
+      }
+      if (!libhashkit_has_algorithm(h)) {
+        WARN("hashkit algorithm not enabled: " << libhashkit_string_hash(h) << " (" << f << ")");
+        continue;
+      }
+
+      REQUIRE(HASHKIT_SUCCESS == stack.set_function(h));
+      REQUIRE(HASHKIT_SUCCESS == hashkit_set_function(&st, h));
+
+      SECTION("can digest set hash function") {
+        auto n = 0;
+
+        for (auto i : input) {
+          CHECK(output[f][n] == stack.digest(LITERAL(i)));
+          CHECK(output[f][n] == hashkit_digest(&st, LITERAL(i)));
+          CHECK(output[f][n] == libhashkit_digest(LITERAL(i), h));
+          ++n;
+        }
+      }
+    }
+  }
+
+  SECTION("is comparable") {
+    REQUIRE(*heap == stack);
+    REQUIRE(hashkit_compare(&st, hp));
+
+    stack.set_function(HASHKIT_HASH_MD5);
+    hashkit_set_function(&st, HASHKIT_HASH_MD5);
+
+    REQUIRE_FALSE(*heap == stack);
+    REQUIRE_FALSE(hashkit_compare(&st, hp));
+  }
+
+  delete heap;
+  hashkit_free(&st);
+  hashkit_free(hp);
+}
diff --git a/testing/lib.cpp b/testing/lib.cpp
new file mode 100644 (file)
index 0000000..571b56b
--- /dev/null
@@ -0,0 +1,60 @@
+#include "lib/common.hpp"
+#include "lib/Cluster.hpp"
+#include "lib/Retry.hpp"
+#include "lib/Server.hpp"
+
+TEST_CASE("lib/Server") {
+  Server server{"memcached"};
+
+  SECTION("starts and listens") {
+
+    REQUIRE(server.start().has_value());
+
+    Retry server_is_listening{[&server] {
+      return server.isListening();
+    }};
+    REQUIRE(server_is_listening());
+
+    SECTION("stops") {
+
+      REQUIRE(server.stop());
+
+      SECTION("is waitable") {
+
+        REQUIRE(server.wait());
+
+        SECTION("stopped") {
+
+          REQUIRE_FALSE(server.check());
+        }
+      }
+    }
+  }
+}
+
+TEST_CASE("lib/Cluster") {
+  Cluster cluster{Server{"memcached", {
+    random_socket_or_port_arg(),
+  }}};
+
+  SECTION("starts and listens") {
+
+    REQUIRE(cluster.start());
+
+    Retry cluster_is_listening{[&cluster] {
+      return cluster.isListening();
+    }};
+    REQUIRE(cluster_is_listening());
+
+    SECTION("stops") {
+
+      cluster.stop();
+      cluster.wait();
+
+      SECTION("stopped") {
+
+        REQUIRE(cluster.isStopped());
+      }
+    }
+  }
+}
index 8a60149568d36832909a8241f920c91f91bdcf8b..5d2ab42a7eb3e5f42fbf18078b3099e086d99bb1 100644 (file)
@@ -2,6 +2,25 @@
 
 #include <sys/wait.h>
 
+Cluster::Cluster(Server &&serv, uint16_t cnt)
+: count{cnt}
+, proto{forward<Server>(serv)}
+{
+  if (!cnt) {
+    count = thread::hardware_concurrency()/2 ?: 4;
+  }
+  reset();
+}
+
+Cluster::~Cluster() {
+  stop();
+  wait();
+}
+
+const vector<Server> &Cluster::getServers() const {
+  return cluster;
+}
+
 void Cluster::reset() {
   pids.clear();
   cluster.clear();
@@ -15,8 +34,10 @@ bool Cluster::start() {
 
   for (auto &server : cluster) {
     auto pid = server.start();
-    if (started &= pid.has_value()) {
+    if (pid.has_value()) {
       pids[*pid] = &server;
+    } else {
+      started = false;
     }
   }
 
@@ -25,7 +46,8 @@ bool Cluster::start() {
 
 void Cluster::stop() {
   for (auto &server : cluster) {
-    server.stop();
+    // no cookies for memcached; TERM is just too slow
+    server.signal(SIGKILL);
   }
 }
 
@@ -38,22 +60,23 @@ bool Cluster::isStopped() {
   return true;
 }
 
-bool Cluster::isListening(int max_timeout) {
-  vector<WaitForConn::conn_t> conns;
-
+bool Cluster::isListening() {
   for (auto &server : cluster) {
-    auto conn = server.createSocket();
-    if (!conn) {
-      return false;
+    if (!server.isListening()) {
+      // zombie?
+      auto old_pid = server.getPid();
+      if (server.tryWait()) {
+        pids.erase(old_pid);
+        auto pid = server.start();
+        if (pid.has_value()) {
+          pids[*pid] = &server;
+        }
+      }
+      return server.isListening();
     }
-    conns.emplace_back(conn.value());
   }
 
-  WaitForConn wait_for_conn{
-    std::move(conns),
-    Poll{POLLOUT, 2, max_timeout}
-  };
-  return wait_for_conn();
+  return true;
 }
 
 void Cluster::wait() {
@@ -65,8 +88,10 @@ void Cluster::wait() {
       return;
     }
 
-    if (pids[inf.si_pid]) {
-      pids[inf.si_pid]->wait();
+    auto server = pids.find(inf.si_pid);
+    if (server != pids.end()) {
+      server->second->wait();
     }
   }
 }
+
index d6a11a7ee79ea65e397d3fa0dfab03b997d88891..416da13849067386af93ad13b34c1b67aba5c3a0 100644 (file)
@@ -1,34 +1,33 @@
 #pragma once
 
+#include "common.hpp"
 #include "Server.hpp"
 
-#include <map>
-#include <thread>
-
 class Cluster {
-private:
-  uint16_t count;
-  Server proto;
-  vector<Server> cluster;
-
-  map<pid_t, Server *> pids;
-
 public:
   explicit
-  Cluster(Server &&serv, uint16_t cnt = 0)
-  : count{cnt}
-  , proto{serv}
-  {
-    if (!cnt) {
-      count = thread::hardware_concurrency() ?: 4;
-    }
-    reset();
-  }
+  Cluster(Server &&serv, uint16_t cnt = 0);
+
+  ~Cluster();
+
+  Cluster(const Cluster &c) = delete;
+  Cluster &operator = (const Cluster &c) = delete;
+
+  Cluster(Cluster &&c) = default;
+  Cluster &operator = (Cluster &&c) = default;
+
+  const vector<Server> &getServers() const;
 
   bool start();
   void stop();
   void reset();
   bool isStopped();
-  bool isListening(int max_timeout = 1000);
+  bool isListening();
   void wait();
+
+private:
+  uint16_t count;
+  Server proto;
+  vector<Server> cluster;
+  map<pid_t, Server *> pids;
 };
diff --git a/testing/lib/Connection.cpp b/testing/lib/Connection.cpp
new file mode 100644 (file)
index 0000000..c4e01d9
--- /dev/null
@@ -0,0 +1,177 @@
+#include "Connection.hpp"
+
+#include <cerrno>
+#include <sys/poll.h>
+#include <unistd.h>
+
+Connection::Connection(socket_or_port_t socket_or_port) {
+  if (holds_alternative<string>(socket_or_port)) {
+    const auto path = get<string>(socket_or_port);
+    const auto safe = path.c_str();
+    const auto zlen = path.length() + 1;
+    const auto ulen = sizeof(sockaddr_un) - sizeof(sa_family_t);
+
+    if (zlen >= ulen) {
+      throw invalid_argument(error({"socket(): path too long '", path, "'"}));
+    }
+
+    if (0 > (sock = socket(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0))) {
+      throw runtime_error(error({"socket(): ", strerror(errno)}));
+    }
+
+    auto sa = reinterpret_cast<sockaddr_un *>(&addr);
+    sa->sun_family = AF_UNIX;
+    copy(safe, safe + zlen, sa->sun_path);
+
+    size = UNIX;
+
+  } else {
+    if (0 > (sock = socket(AF_INET6, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0))) {
+      throw runtime_error(error({"socket(): ", strerror(errno)}));
+    }
+
+    const auto port = get<int>(socket_or_port);
+    auto sa = reinterpret_cast<struct sockaddr_in6 *>(&addr);
+    sa->sin6_family = AF_INET6;
+    sa->sin6_port = htons(static_cast<unsigned short>(port));
+    sa->sin6_addr = IN6ADDR_LOOPBACK_INIT;
+
+    size = INET6;
+  }
+}
+
+Connection::~Connection() {
+  close();
+}
+
+void swap(Connection &a, Connection &b) {
+  a.swap(b);
+}
+
+void Connection::swap(Connection &conn) {
+  Connection copy(conn);
+  conn.sock = sock;
+  conn.addr = addr;
+  conn.size = size;
+  conn.last_err = last_err;
+  sock = exchange(copy.sock, -1);
+  addr = copy.addr;
+  size = copy.size;
+  last_err = copy.last_err;
+}
+
+Connection::Connection(const Connection &conn) {
+  if (conn.sock > -1) {
+    sock = dup(conn.sock);
+  }
+  addr = conn.addr;
+  size = conn.size;
+  last_err = conn.last_err;
+}
+
+Connection &Connection::operator=(const Connection &conn) {
+  Connection copy(conn);
+  copy.swap(*this);
+  return *this;
+}
+
+Connection::Connection(Connection &&conn) noexcept {
+  close();
+  swap(conn);
+}
+
+Connection &Connection::operator=(Connection &&conn) noexcept {
+  Connection copy(forward<Connection>(conn));
+  copy.swap(*this);
+  return *this;
+}
+
+void Connection::close() {
+  if (sock > -1) {
+    ::close(sock);
+    sock = -1;
+    last_err = -1;
+  }
+}
+
+int Connection::getError() {
+  int err = -1;
+  socklen_t len = sizeof(int);
+  if (sock > -1) {
+    errno = 0;
+    if (0 > getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &len)) {
+      err = errno;
+    }
+  }
+  last_err = err;
+  return err;
+}
+
+int Connection::getLastError() {
+  if (last_err == -1) {
+    return getError();
+  }
+  return last_err;
+}
+
+bool Connection::isWritable() {
+  pollfd fd{sock, POLLOUT, 0};
+  if (1 > poll(&fd, 1, 0)) {
+    return false;
+  }
+  if (fd.revents & (POLLNVAL|POLLERR|POLLHUP)) {
+    return false;
+  }
+  return fd.revents & POLLOUT;
+}
+
+bool Connection::isOpen() {
+  if (sock > -1){
+    if (isWritable()) {
+      return getError() == 0;
+    } else if (open()) {
+      if (isWritable()) {
+        return getError() == 0;
+      }
+    }
+  }
+  return false;
+}
+
+bool Connection::open() {
+  if (connected) {
+    return true;
+  }
+  connect_again:
+  errno = 0;
+  if (0 == ::connect(sock, reinterpret_cast<sockaddr *>(&addr), size)) {
+    connected = true;
+    return true;
+  }
+
+  switch (errno) {
+    case EINTR:
+      goto connect_again;
+    case EISCONN:
+      connected = true;
+      [[fallthrough]];
+    case EAGAIN:
+    case EALREADY:
+    case EINPROGRESS:
+      return true;
+
+    default:
+      return false;
+  }
+}
+
+string Connection::error(const initializer_list<string> &args) {
+  stringstream ss;
+
+  for (auto &arg : args) {
+    ss << arg;
+  }
+
+  cerr << ss.str() << endl;
+  return ss.str();
+}
diff --git a/testing/lib/Connection.hpp b/testing/lib/Connection.hpp
new file mode 100644 (file)
index 0000000..6a50a4f
--- /dev/null
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "common.hpp"
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+class Connection {
+public:
+  explicit Connection(socket_or_port_t socket_or_port);
+  ~Connection();
+
+  friend void swap(Connection &a, Connection &b);
+  void swap(Connection &conn);
+
+  Connection(const Connection &conn);
+  Connection &operator = (const Connection &conn);
+
+  Connection(Connection &&conn) noexcept ;
+  Connection &operator = (Connection &&conn) noexcept ;
+
+  int getError();
+  int getLastError();
+
+  bool isWritable();
+  bool isOpen();
+
+  bool open();
+  void close();
+
+private:
+  int sock{-1}, last_err{-1};
+  sockaddr_storage addr{0, {0}, 0};
+  enum sockaddr_size {
+    NONE = 0,
+    UNIX = sizeof(sockaddr_un),
+    INET = sizeof(sockaddr_in),
+    INET6 = sizeof(sockaddr_in6)
+  } size;
+  bool connected{false};
+
+  static string error(const initializer_list<string> &args);
+};
diff --git a/testing/lib/ForkAndExec.cpp b/testing/lib/ForkAndExec.cpp
new file mode 100644 (file)
index 0000000..b882eb4
--- /dev/null
@@ -0,0 +1,54 @@
+#include "ForkAndExec.hpp"
+
+#include <cerrno>
+#include <cstdio>
+
+#include <fcntl.h>
+#include <sys/poll.h>
+#include <unistd.h>
+
+ForkAndExec::ForkAndExec(const char *binary_, char **argv_)
+: binary{binary_}
+, argv{argv_}
+{
+  if (pipe2(pipes, O_CLOEXEC|O_NONBLOCK)) {
+    int error = errno;
+    perror("Server::start pipe2()");
+    throw system_error(error, system_category());
+  }
+}
+
+ForkAndExec::~ForkAndExec() {
+  if (pipes[0] != -1) {
+    close(pipes[0]);
+  }
+  if (pipes[1] != -1) {
+    close(pipes[1]);
+  }
+}
+
+optional<pid_t> ForkAndExec::operator()()  {
+  if (pipes[0] == -1) {
+    return {};
+  }
+  if (pipes[1] != -1) {
+    close(pipes[1]);
+    pipes[1] = -1;
+  }
+
+  switch (pid_t pid = fork()) {
+    case 0:
+      execvp(binary, argv);
+      [[fallthrough]];
+    case -1:
+      perror("fork() && exec()");
+      return {};
+
+    default:
+      pollfd fd{pipes[0], 0, 0};
+      if (1 > poll(&fd, 1, 5000)) {
+        cerr << "exec() timed out" << endl;
+      }
+      return pid;
+  }
+}
diff --git a/testing/lib/ForkAndExec.hpp b/testing/lib/ForkAndExec.hpp
new file mode 100644 (file)
index 0000000..b88c56a
--- /dev/null
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "common.hpp"
+
+class ForkAndExec {
+public:
+  ForkAndExec(const char *binary, char **argv);
+  ~ForkAndExec();
+
+  ForkAndExec(const ForkAndExec &) = delete;
+  ForkAndExec &operator = (const ForkAndExec &) = delete;
+  ForkAndExec(ForkAndExec &&) = default;
+  ForkAndExec &operator = (ForkAndExec &&) = default;
+
+  optional<pid_t> operator () ();
+
+private:
+  int pipes[2];
+  const char *binary;
+  char **argv;
+};
diff --git a/testing/lib/MemcachedCluster.cpp b/testing/lib/MemcachedCluster.cpp
new file mode 100644 (file)
index 0000000..4a33024
--- /dev/null
@@ -0,0 +1,57 @@
+#include "MemcachedCluster.hpp"
+#include "Retry.hpp"
+
+void MemcachedCluster::init() {
+  REQUIRE(cluster.start());
+
+  REQUIRE(memcached_create(&memc));
+  for (const auto &server : cluster.getServers()) {
+    auto target = server.getSocketOrPort();
+    if (holds_alternative<string>(target)) {
+      REQUIRE(MEMCACHED_SUCCESS == memcached_server_add_unix_socket(&memc, get<string>(target).c_str()));
+    } else {
+      REQUIRE(MEMCACHED_SUCCESS == memcached_server_add(&memc, "localhost", get<int>(target)));
+    }
+  }
+
+  Retry cluster_is_listening([this]() {
+    return cluster.isListening();
+  });
+  REQUIRE(cluster_is_listening());
+}
+
+MemcachedCluster::~MemcachedCluster() {
+  memcached_free(&memc);
+}
+
+void MemcachedCluster::flush() {
+  REQUIRE(MEMCACHED_SUCCESS == memcached_flush(&memc, 0));
+}
+
+MemcachedCluster::MemcachedCluster()
+: cluster{Server{getenv_else("MEMCACHED_BINARY", "memcached"), {random_socket_or_port_arg()}}}
+{
+  init();
+}
+
+MemcachedCluster::MemcachedCluster(Cluster &&cluster_)
+: cluster{forward<Cluster>(cluster_)}
+{
+  init();
+}
+
+MemcachedCluster MemcachedCluster::mixed() {
+  return MemcachedCluster{};
+}
+
+MemcachedCluster MemcachedCluster::net() {
+  return MemcachedCluster{Cluster{Server{getenv_else("MEMCACHED_BINARY", "memcached"), {"-p", random_socket_or_port_string}}}};
+}
+
+MemcachedCluster MemcachedCluster::socket() {
+  return MemcachedCluster{Cluster{Server{getenv_else("MEMCACHED_BINARY", "memcached"), {"-s", random_socket_or_port_string}}}};
+}
+
+void MemcachedCluster::enableBinary(bool enable) {
+  REQUIRE(MEMCACHED_SUCCESS == memcached_behavior_set(&memc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, enable));
+}
diff --git a/testing/lib/MemcachedCluster.hpp b/testing/lib/MemcachedCluster.hpp
new file mode 100644 (file)
index 0000000..45fce24
--- /dev/null
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "common.hpp"
+#include "Cluster.hpp"
+
+class MemcachedCluster {
+public:
+  Cluster cluster;
+  memcached_st memc;
+
+  MemcachedCluster();
+  explicit
+  MemcachedCluster(Cluster &&cluster);
+  ~MemcachedCluster();
+
+  void enableBinary(bool enable = true);
+  void flush();
+
+  static MemcachedCluster mixed();
+  static MemcachedCluster net();
+  static MemcachedCluster socket();
+
+private:
+  void init();
+};
diff --git a/testing/lib/Poll.cpp b/testing/lib/Poll.cpp
deleted file mode 100644 (file)
index ac17e72..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-#include "Poll.hpp"
-
-#include <cmath>
-#include <cstdio>
-#include <algorithm>
-#include <iostream>
-
-#include <poll.h>
-
-using namespace std;
-
-bool Poll::operator() (const vector<int> &fds)  {
-  vector<pollfd> pfds;
-
-  pfds.reserve(fds.size());
-  for (auto fd : fds) {
-    pfds.emplace_back(pollfd{fd, events, 0});
-  }
-
-  while (!pfds.empty() && timeout <= max) {
-    auto nfds = poll(pfds.data(), pfds.size(), timeout);
-
-    if (nfds == -1) {
-      perror("Poll::() poll()");
-      return false;
-    }
-
-    /* timeout */
-    if (!nfds) {
-      timeout = ceil(static_cast<float>(timeout) * growth);
-      continue;
-    }
-
-    auto pred = [](const pollfd &pfd){
-      return pfd.revents & POLLHUP
-          || pfd.revents & POLLERR
-          || pfd.revents & pfd.events;
-    };
-    auto iter = remove_if(pfds.begin(), pfds.end(), pred);
-    pfds.erase(iter, pfds.end());
-  }
-
-  return pfds.empty();
-}
diff --git a/testing/lib/Poll.hpp b/testing/lib/Poll.hpp
deleted file mode 100644 (file)
index 0b00e23..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma once
-
-#include <vector>
-
-using namespace std;
-
-class Poll {
-private:
-  short events;
-  int timeout, max;
-  float growth;
-
-public:
-  explicit Poll(short events_ = 0, int timeout_ = 20, int max_ = 1000, float growth_ = 1.1)
-  : events{events_}
-  , timeout{timeout_}
-  , max{max_}
-  , growth{growth_}
-  {}
-
-  bool operator () (const vector<int> &fds);
-};
diff --git a/testing/lib/Retry.cpp b/testing/lib/Retry.cpp
new file mode 100644 (file)
index 0000000..dd6b83c
--- /dev/null
@@ -0,0 +1,22 @@
+#include "Retry.hpp"
+
+Retry::Retry(predicate &&pred_, unsigned int max_, chrono::milliseconds sleep_for_)
+: max{max_}
+, sleep_for{sleep_for_}
+, pred{forward<predicate>(pred_)}
+{}
+
+bool Retry::operator()() {
+  auto cnt = max;
+  auto dur = sleep_for;
+
+  while (cnt--) {
+    if (pred()) {
+      return true;
+    }
+    this_thread::sleep_for(dur);
+    dur *= 2;
+  }
+
+  return false;
+}
diff --git a/testing/lib/Retry.hpp b/testing/lib/Retry.hpp
new file mode 100644 (file)
index 0000000..26924b3
--- /dev/null
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "common.hpp"
+
+class Retry {
+public:
+
+  using predicate = function<bool()>;
+
+  explicit Retry(predicate &&pred_, unsigned max_ = 10, chrono::milliseconds sleep_for_ = 20ms);
+
+  bool operator () ();
+
+private:
+  unsigned max;
+  chrono::milliseconds sleep_for;
+  predicate pred;
+};
+
index bc5de99fe7a0aeee997535b71bafe6b44e863ff7..b3d0118baf9a51f14dbab99101e8f85355ef79f5 100644 (file)
 #include "Server.hpp"
-#include "WaitForExec.hpp"
-#include "WaitForConn.hpp"
+#include "Connection.hpp"
+#include "ForkAndExec.hpp"
 
-#include <netinet/in.h>
-#include <sys/poll.h>
-#include <sys/socket.h>
-#include <sys/un.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
-#include <iostream>
-#include <tuple>
-
-using namespace std;
-
-[[nodiscard]]
-auto Server::createArgv()  {
-  auto i = 0, port = -1, socket = -1;
-  auto arr = new char *[str_args.size() + dyn_args.size()*2 + 2] {
-      strdup(binary.c_str())
-  };
-
-  for (auto &arg : str_args) {
-    arr[++i] = strdup(arg.c_str());
-    if (arg == "-p") {
-      port = i + 1;
-    } else if (arg == "-s") {
-      socket = i + 1;
-    }
-  }
-  for (auto &arg : dyn_args) {
-    arr[++i] = strdup(arg.first.c_str());
-    arr[++i] = strdup(arg.second(arg.first).c_str());
-    if (arg.first == "-p") {
-      port = i;
-    } else if (arg.first == "-s") {
-      socket = i;
-    }
+Server::Server(string &&binary_, Server::argv_t &&args_)
+    : binary{forward<string>(binary_)}
+    , args{forward<argv_t>(args_)}
+{}
 
+Server::~Server() {
+  stop();
+  wait();
+  if (holds_alternative<string>(socket_or_port)) {
+    unlink(get<string>(socket_or_port).c_str());
   }
-  arr[i+1] = nullptr;
+}
 
-  if (socket > -1) {
-    socket_or_port = arr[socket];
-  } else if (port > -1) {
-    socket_or_port = stoi(arr[port]);
+static inline string extractArg(const Server::arg_t &arg_cont, const string &func_arg) {
+  if (holds_alternative<string>(arg_cont)) {
+    return get<string>(arg_cont);
   } else {
-    socket_or_port = 11211;
+    return get<Server::arg_func_t>(arg_cont)(func_arg);
   }
-
-  return arr;
 }
 
-[[nodiscard]]
-optional<WaitForConn::conn_t> Server::createSocket() {
-  sockaddr_storage addr;
-  unsigned size = 0;
-  int sock;
-
-  if (holds_alternative<string>(socket_or_port)) {
-    const auto path = get<string>(socket_or_port);
-    const auto safe = path.c_str();
-    const auto zlen = path.length() + 1;
-    const auto ulen = sizeof(sockaddr_un) - sizeof(sa_family_t);
-
-    if (zlen >= ulen) {
-      cerr << "Server::isListening socket(): path too long '" << path << "'\n";
-      return {};
-    }
-
-    if (0 > (sock = socket(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0))) {
-      perror("Server::isListening socket()");
-      return {};
-    }
+static inline void pushArg(vector<char *> &arr, const string &arg) {
+  auto len = arg.size();
+  auto str = arg.data(), end = str + len + 1;
+  auto ptr = new char[len + 1];
+  copy(str, end, ptr);
+  arr.push_back(ptr);
+}
 
-    auto sa = reinterpret_cast<sockaddr_un *>(&addr);
-    sa->sun_family = AF_UNIX;
-    strncpy(sa->sun_path, safe, zlen);
+optional<string> Server::handleArg(vector<char *> &arr, const string &arg, const arg_func_t &next_arg) {
+  pushArg(arr, arg);
+  if (arg == "-p" || arg == "--port") {
+    auto port = next_arg(arg);
+    pushArg(arr, port);
+    pushArg(arr, "-U");
+    pushArg(arr, port);
+    socket_or_port = stoi(port);
+    return port;
+  } else if (arg == "-s" || arg == "--unix-socket") {
+    auto sock = next_arg(arg);
+    pushArg(arr, sock);
+    socket_or_port = sock;
+    return sock;
+  }
+  return {};
+}
 
-    size = sizeof(*sa);
-  } else {
-    if (0 > (sock = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0))) {
-      perror("Server::isListening socket()");
-      return {};
+[[nodiscard]]
+vector<char *> Server::createArgv()  {
+  vector<char *> arr;
+
+  pushArg(arr, binary);
+  pushArg(arr, "-v");
+
+  for (auto it = args.cbegin(); it != args.cend(); ++it) {
+    if (holds_alternative<arg_t>(*it)) {
+      // a single argument
+      auto arg = extractArg(get<arg_t>(*it), binary);
+      handleArg(arr, arg, [&it](const string &arg_) {
+        return extractArg(get<arg_t>(*++it), arg_);
+      });
+    } else {
+      // an argument pair
+      auto &[one, two] = get<arg_pair_t>(*it);
+      auto arg_one = extractArg(one, binary);
+      auto arg_two = extractArg(two, arg_one);
+
+      auto next = handleArg(arr, arg_one, [&arg_two](const string &) {
+        return arg_two;
+      });
+
+      if (!next.has_value()) {
+        pushArg(arr, arg_two);
+      }
     }
-
-    const auto port = get<int>(socket_or_port);
-    auto sa = reinterpret_cast<struct sockaddr_in *>(&addr);
-    sa->sin_family = AF_INET;
-    sa->sin_port = htons(static_cast<unsigned short>(port)),
-    sa->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
-
-    size = sizeof(*sa);
   }
 
-  return optional<WaitForConn::conn_t>{make_tuple(sock, addr, size)};
+  arr.push_back(nullptr);
+
+  return arr;
 }
 
 optional<pid_t> Server::start() {
@@ -102,36 +92,27 @@ optional<pid_t> Server::start() {
     return pid;
   }
 
-  WaitForExec wait_for_exec;
+  auto argv = createArgv();
+  auto child = ForkAndExec{binary.c_str(), argv.data()}();
 
-  switch (pid = fork()) {
-    case 0:
-      execvp(binary.c_str(), createArgv());
-      [[fallthrough]];
-    case -1:
-      perror("Server::start fork() & exec()");
-      return {};
+  for (auto argp : argv) {
+    delete [] argp;
+  }
 
-    default:
-      if (!wait_for_exec()) {
-        cerr << "Server::start exec(): incomplete\n";
-      }
-      return pid;
+  if (child.has_value()) {
+    pid = child.value();
   }
+
+  return child;
 }
 
-bool Server::isListening(int max_timeout) {
-  auto conn = createSocket();
+bool Server::isListening() {
+  Connection conn(socket_or_port);
 
-  if (!conn) {
+  if (!conn.open()) {
     return false;
   }
-
-  WaitForConn wait_for_conn{
-    {conn.value()},
-    Poll{POLLOUT, 2, max_timeout}
-  };
-  return wait_for_conn();
+  return conn.isOpen();
 }
 
 bool Server::stop() {
@@ -168,3 +149,33 @@ bool Server::wait(int flags) {
 bool Server::tryWait() {
   return wait(WNOHANG);
 }
+
+Server::Server(const Server &s) {
+  binary = s.binary;
+  args = s.args;
+  socket_or_port = s.socket_or_port;
+}
+
+Server &Server::operator=(const Server &s) {
+  binary = s.binary;
+  args = s.args;
+  socket_or_port = s.socket_or_port;
+  return *this;
+}
+
+pid_t Server::getPid() const {
+  return pid;
+}
+
+const string &Server::getBinary() const {
+  return binary;
+}
+
+const Server::argv_t &Server::getArgs() const {
+  return args;
+}
+
+const socket_or_port_t &Server::getSocketOrPort() const {
+  return socket_or_port;
+}
+
index b411f4e34851e20f6f85aba8fd677da574e8cdf7..08ab57734ef28a1512c723c1d157ca29a2b96460 100644 (file)
@@ -1,86 +1,57 @@
 #pragma once
 
-#include "WaitForConn.hpp"
+#include "common.hpp"
 
 #include <csignal>
 
-#include <functional>
-#include <iostream>
-#include <string>
-#include <variant>
-#include <vector>
-
-using namespace std;
-
 class Server {
-  friend class Cluster;
-
 public:
-  using str_args_t = vector<string>;
-  using dyn_args_t = unordered_map<string, function<string(string)>>;
-  using socket_or_port_t = variant<string, int>;
 
-private:
-  string binary;
-  str_args_t str_args;
-  dyn_args_t dyn_args;
-  pid_t pid = 0;
+  friend class Cluster;
 
-  int status = 0;
-  unordered_map<int, unsigned> signalled;
-  socket_or_port_t socket_or_port{11211};
+  using arg_func_t = function<string(string)>;
+  using arg_t = variant<string, arg_func_t>;
+  using arg_pair_t = pair<arg_t, arg_t>;
+  using argv_t = vector<variant<arg_t, arg_pair_t>>;
 
-public:
   explicit
-  Server(string &&binary_, str_args_t &&str_args_ = {}, dyn_args_t &&dyn_args_ = {})
-      : binary{binary_}
-      , str_args{str_args_}
-      , dyn_args{dyn_args_}
-  {}
-
-  Server(string &&binary_, dyn_args_t &&dyn_args_)
-      : binary{binary_}
-      , str_args{}
-      , dyn_args{dyn_args_}
-  {}
-
-  ~Server() {
-    stop();
-    wait();
-  }
-
-  Server &operator = (const Server &s) = default;
-  Server(const Server &s) = default;
+  Server(string &&binary_, argv_t && args_ = {});
+
+  ~Server();
+
+  Server(const Server &s);
+  Server &operator = (const Server &s);
 
   Server &operator = (Server &&s) = default;
   Server(Server &&s) = default;
 
-  pid_t getPid() const {
-    return pid;
-  }
+  pid_t getPid() const;
 
-  const string &getBinary() const {
-    return binary;
-  }
+  const string &getBinary() const;
 
-  const socket_or_port_t &getSocketOrPort() const {
-    return socket_or_port;
-  }
+  const argv_t &getArgs() const;
+
+  const socket_or_port_t &getSocketOrPort() const;
 
   optional<pid_t> start();
   bool stop();
 
   bool signal(int signo = SIGTERM);
   bool check();
-  bool isListening(int max_timeout = 1000);
+  bool isListening();
 
   bool wait(int flags = 0);
   bool tryWait();
 
 private:
-  [[nodiscard]]
-  auto createArgv();
+  string binary;
+  argv_t args;
+  pid_t pid = 0;
+  int status = 0;
+  unordered_map<int, unsigned> signalled;
+  socket_or_port_t socket_or_port = 11211;
 
   [[nodiscard]]
-  optional<WaitForConn::conn_t> createSocket();
+  vector<char *> createArgv();
+  optional<string> handleArg(vector<char *> &arr, const string &arg, const arg_func_t &next_arg);
 };
diff --git a/testing/lib/WaitForConn.cpp b/testing/lib/WaitForConn.cpp
deleted file mode 100644 (file)
index 9d819d6..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-#include "WaitForConn.hpp"
-
-#include <cerrno>
-#include <cstdio>
-#include <sys/socket.h>
-#include <unistd.h>
-
-bool WaitForConn::connect(const conn_t &conn) {
-  int sock;
-  unsigned size;
-  sockaddr_storage addr;
-
-  tie(sock, addr, size) = conn;
-
-  connect_again:
-  if (0 == ::connect(sock, reinterpret_cast<sockaddr *>(&addr), size)) {
-    close(sock);
-    return true;
-  }
-
-  switch (errno) {
-    case EINTR:
-      goto connect_again;
-    case EAGAIN:
-    case EALREADY:
-    case EINPROGRESS:
-    case EISCONN:
-    case ETIMEDOUT:
-      break;
-    default:
-      perror("WaitForConn::connect connect()");
-      close(sock);
-      return false;
-  }
-
-  return true;
-}
-
-bool WaitForConn::operator () () {
-  vector<int> fds;
-
-  fds.reserve(conns.size());
-  for (const auto &conn : conns) {
-    if (!connect(conn)) {
-      return false;
-    }
-    fds.push_back(get<0>(conn));
-  }
-
-  return poll(fds);
-}
diff --git a/testing/lib/WaitForConn.hpp b/testing/lib/WaitForConn.hpp
deleted file mode 100644 (file)
index 758c856..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma once
-
-#include "Poll.hpp"
-
-#include <netinet/in.h>
-#include <sys/poll.h>
-
-#include <tuple>
-#include <vector>
-
-using namespace std;
-
-class WaitForConn {
-public:
-  using conn_t = tuple<int, sockaddr_storage, unsigned int>;
-
-private:
-  vector<conn_t> conns;
-  Poll poll;
-
-public:
-  explicit
-  WaitForConn(vector<conn_t> &&conns_, Poll &&poll_ = Poll{POLLOUT})
-  : conns{conns_}
-  , poll{poll_}
-  { }
-
-  static bool connect(const conn_t &conn);
-
-  bool operator () ();
-};
diff --git a/testing/lib/WaitForExec.cpp b/testing/lib/WaitForExec.cpp
deleted file mode 100644 (file)
index fd25f0f..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-#include "WaitForExec.hpp"
-
-#include <cerrno>
-#include <cstdio>
-
-#include <fcntl.h>
-#include <unistd.h>
-
-#include <system_error>
-
-using namespace std;
-
-WaitForExec::WaitForExec(Poll &&poll_)
-: poll{poll_}
-{
-  if (pipe2(pipes, O_CLOEXEC|O_NONBLOCK)) {
-    int error = errno;
-    perror("Server::start pipe2()");
-    throw system_error(error, system_category());
-  }
-}
-
-WaitForExec::~WaitForExec() {
-  if (pipes[0] != -1) {
-    close(pipes[0]);
-  }
-  if (pipes[1] != -1) {
-    close(pipes[1]);
-  }
-}
-
-bool WaitForExec::operator()()  {
-  if (pipes[0] == -1) {
-    return false;
-  }
-  if (pipes[1] != -1) {
-    close(pipes[1]);
-    pipes[1] = -1;
-  }
-
-  return poll(initializer_list<int>{pipes[0]});
-}
diff --git a/testing/lib/WaitForExec.hpp b/testing/lib/WaitForExec.hpp
deleted file mode 100644 (file)
index d067cbb..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-
-#include "Poll.hpp"
-
-class WaitForExec {
-private:
-  Poll poll;
-  int pipes[2];
-
-public:
-  explicit WaitForExec(Poll &&poll = Poll{0});
-  ~WaitForExec();
-
-  WaitForExec(const WaitForExec &) = delete;
-  WaitForExec(WaitForExec &&) = default;
-
-  bool operator () ();
-};
diff --git a/testing/lib/common.cpp b/testing/lib/common.cpp
new file mode 100644 (file)
index 0000000..16f5e18
--- /dev/null
@@ -0,0 +1,52 @@
+#include "common.hpp"
+#include "Connection.hpp"
+
+#include <cstdlib>
+#include <sys/random.h>
+#include <unistd.h>
+
+unsigned random_num(unsigned min, unsigned max) {
+  unsigned p;
+  getrandom(&p, sizeof(p), 0);
+  return (p % (max - min + 1)) + min;
+}
+
+unsigned random_port() {
+  retry:
+  int port = random_num(2<<9, 2<<15);
+  Connection conn(port);
+
+  if (!conn.open()) {
+    return port;
+  }
+  if (!conn.isOpen()) {
+    return port;
+  }
+  goto retry;
+}
+
+string random_socket() {
+  return "/tmp/libmc." + to_string(random_num(1, UINT32_MAX)) + "@" + to_string(getpid()) + ".sock";
+}
+
+string random_socket_or_port_string(const string &what) {
+  if (what == "-s") {
+    return random_socket();
+  }
+
+  return to_string(random_port());
+}
+
+string random_socket_or_port_flag(const string &binary) {
+  (void) binary;
+  return random_num(0, 1) ? "-p" : "-s";
+}
+
+const char *getenv_else(const char *var, const char *defval) {
+  auto val = getenv(var);
+  if (val && *val) {
+    return val;
+  }
+  return defval;
+}
+
diff --git a/testing/lib/common.hpp b/testing/lib/common.hpp
new file mode 100644 (file)
index 0000000..b516d66
--- /dev/null
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <chrono>
+#include <iostream>
+#include <map>
+#include <optional>
+#include <string>
+#include <sstream>
+#include <stdexcept>
+#include <thread>
+#include <variant>
+#include <vector>
+
+#include "../lib/catch.hpp"
+
+#include "libmemcached/memcached.h"
+
+#define LITERAL(s) (s),strlen(s)
+#define LOOPED_SECTION(tests) \
+  auto i_=0;                  \
+  for (auto &&test : tests) DYNAMIC_SECTION("test" << i_++)
+
+using namespace std;
+
+using socket_or_port_t = variant<string, int>;
+
+const char *getenv_else(const char *var, const char *defval);
+unsigned random_num(unsigned min, unsigned max);
+unsigned random_port();
+string random_socket();
+string random_socket_or_port_string(const string &what);
+string random_socket_or_port_flag(const string &binary);
+
+inline auto random_socket_or_port_arg() {
+  return make_pair(&random_socket_or_port_flag, &random_socket_or_port_string);
+}
+
+class MemcachedPtr {
+public:
+  memcached_st memc;
+
+  explicit
+  MemcachedPtr(memcached_st *memc_) {
+    memset(&memc, 0, sizeof(memc));
+    REQUIRE(memcached_clone(&memc, memc_));
+  }
+  MemcachedPtr()
+  : MemcachedPtr(nullptr)
+  {}
+  ~MemcachedPtr() {
+    memcached_free(&memc);
+  }
+  memcached_st *operator * () {
+    return &memc;
+  }
+};
+
+class Malloced {
+  void *ptr;
+public:
+  Malloced(void *ptr_)
+  : ptr{ptr_}
+  {}
+  ~Malloced() {
+    free(ptr);
+  }
+  void *operator *() {
+    return ptr;
+  }
+};
diff --git a/testing/lib/random_.cpp b/testing/lib/random_.cpp
deleted file mode 100644 (file)
index 3865fb1..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-#include "random_.hpp"
-
-#include <cstdio>
-#include <iostream>
-#include <sys/random.h>
-
-unsigned random_num(unsigned min, unsigned max) {
-  unsigned p;
-  getrandom(&p, sizeof(p), 0);
-  return (p % (max - min + 1)) + min;
-}
-
-unsigned random_port() {
-  return random_num(34567, 65000);
-}
-
-string random_socket() {
-  auto sock = tempnam(nullptr, "libmc");
-
-  if (!sock) {
-    perror("random_socket tempnam()");
-    return {};
-  }
-
-  return sock;
-}
-
-string random_socket_or_port_string(const string &what) {
-  if (what == "-s") {
-    return random_socket();
-  }
-
-  return to_string(random_port());
-}
-
-string random_socket_or_port_flag() {
-  return random_num(0, 1) ? "-p" : "-s";
-}
diff --git a/testing/lib/random_.hpp b/testing/lib/random_.hpp
deleted file mode 100644 (file)
index f3f28b5..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include <string>
-#include <tuple>
-#include "Server.hpp"
-
-using namespace std;
-
-unsigned random_num(unsigned min, unsigned max);
-unsigned random_port();
-string random_socket();
-string random_socket_or_port_string(const string &what);
-string random_socket_or_port_flag();
-
-inline Server::dyn_args_t::value_type random_socket_or_port_arg() {
-  return {random_socket_or_port_flag(), &random_socket_or_port_string};
-}
diff --git a/testing/memcached/basic.cpp b/testing/memcached/basic.cpp
new file mode 100644 (file)
index 0000000..8e6cced
--- /dev/null
@@ -0,0 +1,28 @@
+#include "../lib/common.hpp"
+
+#include "libmemcached/is.h"
+
+TEST_CASE("memcached basic") {
+  memcached_st memc, *memc_ptr;
+
+  memc_ptr = memcached_create(&memc);
+
+  REQUIRE(memc_ptr);
+  REQUIRE(memc_ptr == &memc);
+
+  SECTION("can be cloned") {
+    memc_ptr = memcached_clone(nullptr, &memc);
+    REQUIRE(memc_ptr);
+    REQUIRE(memcached_is_allocated(memc_ptr));
+    memcached_free(memc_ptr);
+  }
+
+  SECTION("can be reset") {
+    memc_ptr = memcached_clone(nullptr, &memc);
+    REQUIRE(MEMCACHED_SUCCESS == memcached_reset(&memc));
+    REQUIRE_FALSE(memcached_is_allocated(&memc));
+    REQUIRE(MEMCACHED_SUCCESS == memcached_reset(memc_ptr));
+    REQUIRE(memcached_is_allocated(memc_ptr));
+    memcached_free(memc_ptr);
+  }
+}
diff --git a/testing/memcached/callbacks.cpp b/testing/memcached/callbacks.cpp
new file mode 100644 (file)
index 0000000..63cc0ed
--- /dev/null
@@ -0,0 +1,19 @@
+#include "../lib/common.hpp"
+
+static memcached_return_t delete_trigger(memcached_st *, const char *, size_t) {
+  return MEMCACHED_SUCCESS;
+}
+
+TEST_CASE("memcached callbacks") {
+  void *fptr = reinterpret_cast<void *>(reinterpret_cast<intptr_t>(&delete_trigger));
+  MemcachedPtr memc;
+
+  SECTION("set delete trigger") {
+    REQUIRE(MEMCACHED_SUCCESS == memcached_callback_set(*memc, MEMCACHED_CALLBACK_DELETE_TRIGGER, fptr));
+  }
+
+  SECTION("set delete trigger fails w/ NOREPLY") {
+    REQUIRE(MEMCACHED_SUCCESS == memcached_behavior_set(*memc, MEMCACHED_BEHAVIOR_NOREPLY, true));
+    REQUIRE_FALSE(MEMCACHED_SUCCESS == memcached_callback_set(*memc, MEMCACHED_CALLBACK_DELETE_TRIGGER, fptr));
+  }
+}
diff --git a/testing/memcached/dump.cpp b/testing/memcached/dump.cpp
new file mode 100644 (file)
index 0000000..016daba
--- /dev/null
@@ -0,0 +1,47 @@
+#include "../lib/common.hpp"
+#include "../lib/MemcachedCluster.hpp"
+
+memcached_return_t dump_cb(const memcached_st *, const char *, size_t, void *ctx) {
+  size_t *c = reinterpret_cast<size_t *>(ctx);
+  ++(*c);
+  return MEMCACHED_SUCCESS;
+}
+
+TEST_CASE("memcached dump") {
+  MemcachedCluster tests[]{
+      MemcachedCluster::mixed(),
+      MemcachedCluster::net(),
+      MemcachedCluster::socket()
+  };
+
+  LOOPED_SECTION(tests) {
+    auto memc = &test.memc;
+
+    SECTION("prepared with 64 KVs") {
+      for (int i = 0; i < 64; ++i) {
+        char key[8];
+        int len = snprintf(key, sizeof(key) - 1, "k_%d", i);
+
+        CHECKED_IF(len) {
+          auto rc = memcached_set(memc, key, len, key, len, 0, 0);
+          INFO("last error: " << memcached_last_error(memc));
+          REQUIRE(MEMCACHED_SUCCESS == rc);
+        }
+      }
+
+      memcached_quit(memc);
+
+      // let memcached sort itself
+      using namespace chrono_literals;
+      this_thread::sleep_for(3s);
+
+      SECTION("dumps 64 KVs") {
+        size_t counter = 0;
+        memcached_dump_fn fn[] = {dump_cb};
+
+        REQUIRE(MEMCACHED_SUCCESS == memcached_dump(memc, fn, &counter, 1));
+        REQUIRE(counter == 64);
+      }
+    }
+  }
+}
diff --git a/testing/memcached/encoding_key.cpp b/testing/memcached/encoding_key.cpp
new file mode 100644 (file)
index 0000000..cb7620a
--- /dev/null
@@ -0,0 +1,104 @@
+#include "../lib/common.hpp"
+#include "../lib/MemcachedCluster.hpp"
+
+#define TEST_KEY LITERAL("test")
+#define INITIAL_VAL LITERAL("initial")
+#define REPLACED_VAL LITERAL("replaced")
+
+static inline void check(memcached_st *enc, memcached_st *raw, const char *val, size_t len) {
+  memcached_return_t enc_rc, raw_rc;
+  size_t enc_length, raw_length;
+  Malloced enc_value(memcached_get(enc, TEST_KEY, &enc_length, nullptr, &enc_rc));
+  Malloced raw_value(memcached_get(raw, TEST_KEY, &raw_length, nullptr, &raw_rc));
+
+  REQUIRE(enc_rc == MEMCACHED_SUCCESS);
+  REQUIRE(raw_rc == MEMCACHED_SUCCESS);
+  REQUIRE_FALSE(enc_length == raw_length);
+  REQUIRE(memcmp(*raw_value, *enc_value, raw_length));
+  REQUIRE(enc_length == len);
+  REQUIRE_FALSE(memcmp(val, *enc_value, enc_length));
+}
+
+TEST_CASE("memcached encoding_key") {
+  MemcachedCluster tests[]{
+      MemcachedCluster::mixed(),
+      MemcachedCluster::net(),
+      MemcachedCluster::socket()
+  };
+
+  LOOPED_SECTION(tests) {
+    auto memc = &test.memc;
+
+    SECTION("accepts encoding key") {
+      MemcachedPtr copy(memc);
+
+      REQUIRE(MEMCACHED_SUCCESS ==
+              memcached_set_encoding_key(memc, LITERAL(__func__)));
+
+      SECTION("sets encoded value") {
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_set(memc, TEST_KEY, INITIAL_VAL, 0, 0));
+
+        SECTION("gets encoded value") {
+          check(memc, &copy.memc, INITIAL_VAL);
+        }
+
+        SECTION("cloned gets encoded value") {
+          MemcachedPtr dupe(memc);
+
+          check(&dupe.memc, &copy.memc, INITIAL_VAL);
+        }
+      }
+
+      SECTION("adds encoded value") {
+
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_set(memc, TEST_KEY, INITIAL_VAL, 0, 0));
+        REQUIRE(MEMCACHED_NOTSTORED ==
+                memcached_add(memc, TEST_KEY, REPLACED_VAL, 0, 0));
+
+        check(memc, &copy.memc, INITIAL_VAL);
+
+        test.flush();
+
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_add(memc, TEST_KEY, REPLACED_VAL, 0, 0));
+
+        SECTION("gets encoded value") {
+          check(memc, &copy.memc, REPLACED_VAL);
+        }
+      }
+
+      SECTION("replaces encoded value") {
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_set(memc, TEST_KEY, INITIAL_VAL, 0, 0));
+
+        check(memc, &copy.memc, INITIAL_VAL);
+
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_replace(memc, TEST_KEY, REPLACED_VAL, 0, 0));
+
+        SECTION("gets encoded value") {
+          check(memc, &copy.memc, REPLACED_VAL);
+        }
+      }
+
+      SECTION("unsupported") {
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_increment(memc, TEST_KEY, 0, nullptr));
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_decrement(memc, TEST_KEY, 0, nullptr));
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_increment_with_initial(memc, TEST_KEY, 0, 0,
+                                                 0, nullptr));
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_decrement_with_initial(memc, TEST_KEY, 0, 0,
+                                                 0, nullptr));
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_append(memc, TEST_KEY, REPLACED_VAL, 0, 0));
+        REQUIRE(MEMCACHED_NOT_SUPPORTED ==
+                memcached_prepend(memc, TEST_KEY, REPLACED_VAL, 0, 0));
+      }
+    }
+  }
+}
diff --git a/testing/memcached/exist.cpp b/testing/memcached/exist.cpp
new file mode 100644 (file)
index 0000000..663deec
--- /dev/null
@@ -0,0 +1,62 @@
+#include "../lib/common.hpp"
+#include "../lib/MemcachedCluster.hpp"
+
+TEST_CASE("memcached exist") {
+  MemcachedCluster tests[]{
+      MemcachedCluster::mixed(),
+      MemcachedCluster::net(),
+      MemcachedCluster::socket()
+  };
+
+  tests[0].enableBinary();
+
+  LOOPED_SECTION(tests) {
+    auto memc = &test.memc;
+
+    SECTION("initial not found") {
+      REQUIRE(
+          MEMCACHED_NOTFOUND == memcached_exist(memc, LITERAL("frog")));
+    }
+
+    SECTION("set found") {
+      REQUIRE(MEMCACHED_SUCCESS ==
+              memcached_set(memc, LITERAL("frog"), LITERAL("frog"), 0,
+                            0));
+      REQUIRE(
+          MEMCACHED_SUCCESS == memcached_exist(memc, LITERAL("frog")));
+
+      SECTION("deleted not found") {
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_delete(memc, LITERAL("frog"), 0));
+        REQUIRE(MEMCACHED_NOTFOUND ==
+                memcached_exist(memc, LITERAL("frog")));
+      }
+    }
+
+    SECTION("by key") {
+      SECTION("initial not found") {
+        REQUIRE(MEMCACHED_NOTFOUND ==
+                memcached_exist_by_key(memc, LITERAL("master"),
+                                       LITERAL("frog")));
+      }
+
+      SECTION("set found") {
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_set_by_key(memc, LITERAL("master"),
+                                     LITERAL("frog"), LITERAL("frog"), 0, 0));
+        REQUIRE(MEMCACHED_SUCCESS ==
+                memcached_exist_by_key(memc, LITERAL("master"),
+                                       LITERAL("frog")));
+
+        SECTION("deleted not found") {
+          REQUIRE(MEMCACHED_SUCCESS ==
+                  memcached_delete_by_key(memc, LITERAL("master"),
+                                          LITERAL("frog"), 0));
+          REQUIRE(MEMCACHED_NOTFOUND ==
+                  memcached_exist_by_key(memc, LITERAL("master"),
+                                         LITERAL("frog")));
+        }
+      }
+    }
+  }
+}
diff --git a/testing/memcached/servers.cpp b/testing/memcached/servers.cpp
new file mode 100644 (file)
index 0000000..69f76bd
--- /dev/null
@@ -0,0 +1,40 @@
+#include "../lib/common.hpp"
+
+TEST_CASE("memcached servers") {
+  SECTION("memcached_servers_parse") {
+    SECTION("does not leak memory") {
+      memcached_server_st *s = memcached_servers_parse("1.2.3.4:1234");
+      REQUIRE(s);
+      memcached_server_free(s);
+    }
+  }
+
+  SECTION("memcached_server_list") {
+    SECTION("append with weight - all zeros") {
+      memcached_server_st *sl = memcached_server_list_append_with_weight(
+          nullptr, nullptr, 0, 0, 0);
+      REQUIRE(sl);
+      memcached_server_list_free(sl);
+    }
+    SECTION("append with weight - host set only") {
+      memcached_server_st *sl = memcached_server_list_append_with_weight(
+          nullptr, "localhost", 0, 0, 0);
+      REQUIRE(sl);
+      memcached_server_list_free(sl);
+    }
+    SECTION("append with weight - error set only") {
+      memcached_return_t rc;
+      memcached_server_st *sl = memcached_server_list_append_with_weight(
+          nullptr, nullptr, 0, 0, &rc);
+      REQUIRE(sl);
+      REQUIRE(MEMCACHED_SUCCESS == rc);
+      memcached_server_list_free(sl);
+    }
+  }
+
+  SECTION("no configured servers") {
+    MemcachedPtr memc;
+
+    REQUIRE(MEMCACHED_NO_SERVERS == memcached_increment(*memc, LITERAL("key"), 1, nullptr));
+  }
+}
diff --git a/testing/server.cpp b/testing/server.cpp
deleted file mode 100644 (file)
index 0d2469d..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-#include "lib/catch.hpp"
-#include "lib/Cluster.hpp"
-
-#include "lib/random_.hpp"
-
-TEST_CASE("Server") {
-  Server server{"memcached"};
-
-  SECTION("starts and listens") {
-
-    REQUIRE(server.start().has_value());
-    REQUIRE(server.isListening());
-
-    SECTION("stops") {
-
-      REQUIRE(server.stop());
-
-      SECTION("is waitable") {
-
-        REQUIRE(server.wait());
-
-        SECTION("stopped") {
-          REQUIRE(server.is)
-        }
-      }
-    }
-  }
-}
-
-TEST_CASE("Cluster") {
-  Cluster cluster{Server{"memcached", {
-    random_socket_or_port_arg(),
-  }}};
-
-  SECTION("starts and listens") {
-
-    REQUIRE(cluster.start());
-    REQUIRE(cluster.isListening());
-
-    SECTION("stops") {
-
-      cluster.stop();
-      cluster.wait();
-
-      SECTION("stopped") {
-
-        REQUIRE(cluster.isStopped());
-      }
-    }
-  }
-}