From 3030a82e4995d0d31c2357cfd74e9b5a0c51de9b Mon Sep 17 00:00:00 2001
From: Lee *!* Clagett <vtnerd@users.noreply.github.com>
Date: Tue, 17 Jan 2023 14:10:24 -0500
Subject: [PATCH] Added unit tests, and fixed two bugs: (#53)

* Integer conversion checks in src/wire/read.h
 * Missing "boolean" function in wire::writer and derived types
---
 CMakeLists.txt                           |    7 +
 src/rpc/light_wallet.cpp                 |    4 +-
 src/wire/json/read.cpp                   |   15 +-
 src/wire/json/write.cpp                  |    6 +
 src/wire/json/write.h                    |    2 +
 src/wire/read.cpp                        |   11 +-
 src/wire/read.h                          |  109 +-
 src/wire/write.h                         |    7 +
 tests/CMakeLists.txt                     |   29 +
 tests/unit/CMakeLists.txt                |   37 +
 tests/unit/framework.test.cpp            |   37 +
 tests/unit/framework.test.h              |   39 +
 tests/unit/lest.hpp                      | 1484 ++++++++++++++++++++++
 tests/unit/main.cpp                      |   33 +
 tests/unit/wire/CMakeLists.txt           |   39 +
 tests/unit/wire/base.test.h              |   54 +
 tests/unit/wire/json/CMakeLists.txt      |   37 +
 tests/unit/wire/json/read.write.test.cpp |  109 ++
 tests/unit/wire/read.test.cpp            |  107 ++
 tests/unit/wire/read.write.test.cpp      |  230 ++++
 20 files changed, 2326 insertions(+), 70 deletions(-)
 create mode 100644 tests/CMakeLists.txt
 create mode 100644 tests/unit/CMakeLists.txt
 create mode 100644 tests/unit/framework.test.cpp
 create mode 100644 tests/unit/framework.test.h
 create mode 100644 tests/unit/lest.hpp
 create mode 100644 tests/unit/main.cpp
 create mode 100644 tests/unit/wire/CMakeLists.txt
 create mode 100644 tests/unit/wire/base.test.h
 create mode 100644 tests/unit/wire/json/CMakeLists.txt
 create mode 100644 tests/unit/wire/json/read.write.test.cpp
 create mode 100644 tests/unit/wire/read.test.cpp
 create mode 100644 tests/unit/wire/read.write.test.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 65ffdd5..4635c84 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,6 +33,8 @@ enable_language(CXX)
 set(CMAKE_CXX_STANDARD 14)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
+option(BUILD_TESTS "Build Tests" OFF)
+
 set(MONERO_LIBRARIES
   daemon_messages
   serialization
@@ -247,3 +249,8 @@ set_property(TARGET monero::libraries PROPERTY
 #
 
 add_subdirectory(src)
+
+if (BUILD_TESTS)
+  enable_testing()
+  add_subdirectory(tests)
+endif()
diff --git a/src/rpc/light_wallet.cpp b/src/rpc/light_wallet.cpp
index e5b5a57..b6c04df 100644
--- a/src/rpc/light_wallet.cpp
+++ b/src/rpc/light_wallet.cpp
@@ -165,7 +165,7 @@ namespace lws
 
   void rpc::read_bytes(wire::json_reader& source, safe_uint64& self)
   {
-    self = safe_uint64(wire::integer::convert_to<std::uint64_t>(source.safe_unsigned_integer()));
+    self = safe_uint64(wire::integer::cast_unsigned<std::uint64_t>(source.safe_unsigned_integer()));
   }
   void rpc::write_bytes(wire::json_writer& dest, const safe_uint64 self)
   {
@@ -175,7 +175,7 @@ namespace lws
   void rpc::read_bytes(wire::json_reader& source, safe_uint64_array& self)
   {
     for (std::size_t count = source.start_array(); !source.is_array_end(count); --count)
-      self.values.emplace_back(wire::integer::convert_to<std::uint64_t>(source.safe_unsigned_integer()));
+      self.values.emplace_back(wire::integer::cast_unsigned<std::uint64_t>(source.safe_unsigned_integer()));
     source.end_array();
   }
 
diff --git a/src/wire/json/read.cpp b/src/wire/json/read.cpp
index 497c278..f776f6f 100644
--- a/src/wire/json/read.cpp
+++ b/src/wire/json/read.cpp
@@ -233,13 +233,22 @@ namespace wire
     return json_bool.value.boolean;
   }
 
+  using imax_limits = std::numeric_limits<std::intmax_t>;
+  static_assert(0 <= imax_limits::max(), "expected 0 <= intmax_t::max");
+  static_assert(
+    imax_limits::max() <= std::numeric_limits<std::uintmax_t>::max(),
+    "expected intmax_t::max <= uintmax_t::max"
+  );
+
   std::intmax_t json_reader::integer()
   {
     rapidjson_sax json_int{error::schema::integer};
     read_next_value(json_int);
     if (json_int.negative)
       return json_int.value.integer;
-    return integer::convert_to<std::intmax_t>(json_int.value.unsigned_integer);
+    if (static_cast<std::uintmax_t>(imax_limits::max()) < json_int.value.unsigned_integer)
+      WIRE_DLOG_THROW_(error::schema::smaller_integer);
+    return static_cast<std::intmax_t>(json_int.value.unsigned_integer);
   }
 
   std::uintmax_t json_reader::unsigned_integer()
@@ -248,7 +257,9 @@ namespace wire
     read_next_value(json_uint);
     if (!json_uint.negative)
       return json_uint.value.unsigned_integer;
-    return integer::convert_to<std::uintmax_t>(json_uint.value.integer);
+    if (json_uint.value.integer < 0)
+      WIRE_DLOG_THROW_(error::schema::larger_integer);
+    return static_cast<std::uintmax_t>(json_uint.value.integer);
   }
     /*
   const std::vector<std::uintmax_t>& json_reader::unsigned_integer_array()
diff --git a/src/wire/json/write.cpp b/src/wire/json/write.cpp
index ce830ec..d852d7f 100644
--- a/src/wire/json/write.cpp
+++ b/src/wire/json/write.cpp
@@ -73,6 +73,12 @@ namespace wire
     return buf;
   }
 
+  void json_writer::boolean(const bool source)
+  {
+    formatter_.Bool(source);
+    check_flush();
+  }
+
   void json_writer::integer(const int source)
   {
     formatter_.Int(source);
diff --git a/src/wire/json/write.h b/src/wire/json/write.h
index bea13f7..ee8ad40 100644
--- a/src/wire/json/write.h
+++ b/src/wire/json/write.h
@@ -85,6 +85,8 @@ namespace wire
     //! \return Null-terminated buffer containing uint as decimal ascii
     static std::array<char, uint_to_string_size> to_string(std::uintmax_t) noexcept;
 
+    void boolean(bool) override final;
+
     void integer(int) override final;
     void integer(std::intmax_t) override final;
 
diff --git a/src/wire/read.cpp b/src/wire/read.cpp
index 85d2977..def3d2f 100644
--- a/src/wire/read.cpp
+++ b/src/wire/read.cpp
@@ -35,9 +35,16 @@ void wire::reader::increment_depth()
     WIRE_DLOG_THROW_(error::schema::maximum_depth);
 }
 
-[[noreturn]] void wire::integer::throw_exception(std::intmax_t source, std::intmax_t min)
+[[noreturn]] void wire::integer::throw_exception(std::intmax_t source, std::intmax_t min, std::intmax_t max)
 {
-  WIRE_DLOG_THROW(error::schema::larger_integer, source << " given when " << min << " is minimum permitted");
+  static_assert(
+    std::numeric_limits<std::intmax_t>::max() <= std::numeric_limits<std::uintmax_t>::max(),
+    "expected intmax_t::max <= uintmax_t::max"
+  );
+  if (source < 0)
+    WIRE_DLOG_THROW(error::schema::larger_integer, source << " given when " << min << " is minimum permitted");
+  else
+    throw_exception(std::uintmax_t(source), std::uintmax_t(max));
 }
 [[noreturn]] void wire::integer::throw_exception(std::uintmax_t source, std::uintmax_t max)
 {
diff --git a/src/wire/read.h b/src/wire/read.h
index bc5ce1b..a73560d 100644
--- a/src/wire/read.h
+++ b/src/wire/read.h
@@ -83,7 +83,7 @@ namespace wire
     //! \throw wire::exception if next value not a boolean.
     virtual bool boolean() = 0;
 
-    //! \throw wire::expception if next value not an integer.
+    //! \throw wire::exception if next value not an integer.
     virtual std::intmax_t integer() = 0;
 
     //! \throw wire::exception if next value not an unsigned integer.
@@ -104,8 +104,7 @@ namespace wire
     //! \throw wire::exception if next value invalid enum. \return Index in `enums`.
     virtual std::size_t enumeration(epee::span<char const* const> enums) = 0;
 
-    /*! \throw wire::exception if next value not array
-        \return Number of values to read before calling `is_array_end()`. */
+    //! \throw wire::exception if next value not array
     virtual std::size_t start_array() = 0;
 
     //! \return True if there is another element to read.
@@ -167,76 +166,58 @@ namespace wire
 
   namespace integer
   {
-    [[noreturn]] void throw_exception(std::intmax_t source, std::intmax_t min);
-    [[noreturn]] void throw_exception(std::uintmax_t source, std::uintmax_t max);
+    [[noreturn]] void throw_exception(std::intmax_t value, std::intmax_t min, std::intmax_t max);
+    [[noreturn]] void throw_exception(std::uintmax_t value, std::uintmax_t max);
 
-    template<typename Target, typename U>
-    inline Target convert_to(const U source)
+    template<typename T, typename U>
+    inline T cast_signed(const U source)
     {
-      using common = typename std::common_type<Target, U>::type;
-      static constexpr const Target target_min = std::numeric_limits<Target>::min();
-      static constexpr const Target target_max = std::numeric_limits<Target>::max();
+      using limit = std::numeric_limits<T>;
+      static_assert(
+        std::is_signed<T>::value && std::is_integral<T>::value,
+        "target must be signed integer type"
+      );
+      static_assert(
+        std::is_signed<U>::value && std::is_integral<U>::value,
+        "source must be signed integer type"
+      );
+      if (source < limit::min() || limit::max() < source)
+       throw_exception(source, limit::min(), limit::max());
+      return static_cast<T>(source);
+    }
 
-      /* After optimizations, this is:
-           * 1 check for unsigned -> unsigned (uint, uint)
-           * 2 checks for signed -> signed (int, int)
-           * 2 checks for signed -> unsigned-- (
-           * 1 check for unsigned -> signed (uint, uint)
-
-         Put `WIRE_DLOG_THROW` in cpp to reduce code/ASM duplication. Do not
-         remove first check, signed values can be implicitly converted to
-         unsigned in some checks. */
-      if (!std::numeric_limits<Target>::is_signed && source < 0)
-        throw_exception(std::intmax_t(source), std::intmax_t(0));
-      else if (common(source) < common(target_min))
-        throw_exception(std::intmax_t(source), std::intmax_t(target_min));
-      else if (common(target_max) < common(source))
-        throw_exception(std::uintmax_t(source), std::uintmax_t(target_max));
-
-      return Target(source);
+    template<typename T, typename U>
+    inline T cast_unsigned(const U source)
+    {
+      using limit = std::numeric_limits<T>;
+      static_assert(
+        std::is_unsigned<T>::value && std::is_integral<T>::value,
+        "target must be unsigned integer type"
+      );
+      static_assert(
+        std::is_unsigned<U>::value && std::is_integral<U>::value,
+        "source must be unsigned integer type"
+      );
+      if (limit::max() < source)
+        throw_exception(source, limit::max());
+      return static_cast<T>(source);
     }
   }
 
-  inline void read_bytes(reader& source, char& dest)
+  //! read all current and future signed integer types
+  template<typename T>
+  inline enable_if<std::is_signed<T>::value && std::is_integral<T>::value>
+  read_bytes(reader& source, T& dest)
   {
-    dest = integer::convert_to<char>(source.integer());
-  }
-  inline void read_bytes(reader& source, short& dest)
-  {
-    dest = integer::convert_to<short>(source.integer());
-  }
-  inline void read_bytes(reader& source, int& dest)
-  {
-    dest = integer::convert_to<int>(source.integer());
-  }
-  inline void read_bytes(reader& source, long& dest)
-  {
-    dest = integer::convert_to<long>(source.integer());
-  }
-  inline void read_bytes(reader& source, long long& dest)
-  {
-    dest = integer::convert_to<long long>(source.integer());
+    dest = integer::cast_signed<T>(source.integer());
   }
 
-  inline void read_bytes(reader& source, unsigned char& dest)
+  //! read all current and future unsigned integer types
+  template<typename T>
+  inline enable_if<std::is_unsigned<T>::value && std::is_integral<T>::value>
+  read_bytes(reader& source, T& dest)
   {
-    dest = integer::convert_to<unsigned char>(source.unsigned_integer());
-  }
-  inline void read_bytes(reader& source, unsigned short& dest)
-  {
-    dest = integer::convert_to<unsigned short>(source.unsigned_integer());
-  }
-  inline void read_bytes(reader& source, unsigned& dest)
-  {
-    dest = integer::convert_to<unsigned>(source.unsigned_integer());
-  }
-  inline void read_bytes(reader& source, unsigned long& dest)
-  {
-    dest = integer::convert_to<unsigned long>(source.unsigned_integer());
-  }
-  inline void read_bytes(reader& source, unsigned long long& dest)
-  {
-    dest = integer::convert_to<unsigned long long>(source.unsigned_integer());
+    dest = integer::cast_unsigned<T>(source.unsigned_integer());
   }
 } // wire
 
@@ -273,7 +254,7 @@ namespace wire_read
     using value_type = typename T::value_type;
     static_assert(!std::is_same<value_type, char>::value, "read array of chars as binary");
     static_assert(!std::is_same<value_type, std::uint8_t>::value, "read array of unsigned chars as binary");
-    
+
     std::size_t count = source.start_array();
 
     dest.clear();
diff --git a/src/wire/write.h b/src/wire/write.h
index 696d933..c89afaa 100644
--- a/src/wire/write.h
+++ b/src/wire/write.h
@@ -47,6 +47,8 @@ namespace wire
 
     virtual ~writer() noexcept;
 
+    virtual void boolean(bool) = 0;
+
     virtual void integer(int) = 0;
     virtual void integer(std::intmax_t) = 0;
 
@@ -78,6 +80,11 @@ namespace wire
 
   // leave in header, compiler can de-virtualize when final type is given
 
+  inline void write_bytes(writer& dest, const bool source)
+  {
+    dest.boolean(source);
+  }
+
   inline void write_bytes(writer& dest, const int source)
   {
     dest.integer(source);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 0000000..181bee5
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,29 @@
+# Copyright (c) 2022, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+add_subdirectory(unit)
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
new file mode 100644
index 0000000..3412c12
--- /dev/null
+++ b/tests/unit/CMakeLists.txt
@@ -0,0 +1,37 @@
+# Copyright (c) 2022, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+add_library(monero-lws-unit-framework framework.test.cpp)
+target_include_directories(monero-lws-unit-framework PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src")
+target_link_libraries(monero-lws-unit-framework)
+
+add_subdirectory(wire)
+
+add_executable(monero-lws-unit main.cpp)
+target_link_libraries(monero-lws-unit monero-lws-unit-framework monero-lws-unit-wire monero-lws-unit-wire-json)
+add_test(NAME monero-lws-unit COMMAND monero-lws-unit -v)
diff --git a/tests/unit/framework.test.cpp b/tests/unit/framework.test.cpp
new file mode 100644
index 0000000..bc11aa5
--- /dev/null
+++ b/tests/unit/framework.test.cpp
@@ -0,0 +1,37 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "framework.test.h"
+
+namespace lws_test
+{
+  lest::tests& get_tests()
+  {
+    static lest::tests instance;
+    return instance;
+  }
+}
diff --git a/tests/unit/framework.test.h b/tests/unit/framework.test.h
new file mode 100644
index 0000000..5b0b2b5
--- /dev/null
+++ b/tests/unit/framework.test.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#define lest_FEATURE_AUTO_REGISTER 1
+#include "lest.hpp"
+
+#define LWS_CASE(name) \
+  lest_CASE(lws_test::get_tests(), name)
+
+namespace lws_test
+{
+  lest::tests& get_tests();
+}
diff --git a/tests/unit/lest.hpp b/tests/unit/lest.hpp
new file mode 100644
index 0000000..f41942c
--- /dev/null
+++ b/tests/unit/lest.hpp
@@ -0,0 +1,1484 @@
+// Copyright 2013-2018 by Martin Moene
+//
+// lest is based on ideas by Kevlin Henney, see video at
+// http://skillsmatter.com/podcast/agile-testing/kevlin-henney-rethinking-unit-testing-in-c-plus-plus
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef LEST_LEST_HPP_INCLUDED
+#define LEST_LEST_HPP_INCLUDED
+
+#include <algorithm>
+#include <chrono>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <iterator>
+#include <limits>
+#include <random>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <set>
+#include <tuple>
+#include <typeinfo>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include <cctype>
+#include <cmath>
+#include <cstddef>
+
+#define lest_MAJOR  1
+#define lest_MINOR  35
+#define lest_PATCH  1
+
+#define  lest_VERSION  lest_STRINGIFY(lest_MAJOR) "." lest_STRINGIFY(lest_MINOR) "." lest_STRINGIFY(lest_PATCH)
+
+#ifndef  lest_FEATURE_AUTO_REGISTER
+# define lest_FEATURE_AUTO_REGISTER  0
+#endif
+
+#ifndef  lest_FEATURE_COLOURISE
+# define lest_FEATURE_COLOURISE  0
+#endif
+
+#ifndef  lest_FEATURE_LITERAL_SUFFIX
+# define lest_FEATURE_LITERAL_SUFFIX  0
+#endif
+
+#ifndef  lest_FEATURE_REGEX_SEARCH
+# define lest_FEATURE_REGEX_SEARCH  0
+#endif
+
+#ifndef  lest_FEATURE_TIME_PRECISION
+# define lest_FEATURE_TIME_PRECISION  0
+#endif
+
+#ifndef  lest_FEATURE_WSTRING
+# define lest_FEATURE_WSTRING  1
+#endif
+
+#ifdef    lest_FEATURE_RTTI
+# define  lest__cpp_rtti  lest_FEATURE_RTTI
+#elif defined(__cpp_rtti)
+# define  lest__cpp_rtti  __cpp_rtti
+#elif defined(__GXX_RTTI) || defined (_CPPRTTI)
+# define  lest__cpp_rtti  1
+#else
+# define  lest__cpp_rtti  0
+#endif
+
+#if lest_FEATURE_REGEX_SEARCH
+# include <regex>
+#endif
+
+// Stringify:
+
+#define lest_STRINGIFY(  x )  lest_STRINGIFY_( x )
+#define lest_STRINGIFY_( x )  #x
+
+// Compiler warning suppression:
+
+#if defined (__clang__)
+# pragma clang diagnostic ignored "-Waggregate-return"
+# pragma clang diagnostic ignored "-Woverloaded-shift-op-parentheses"
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunused-comparison"
+#elif defined (__GNUC__)
+# pragma GCC   diagnostic ignored "-Waggregate-return"
+# pragma GCC   diagnostic push
+#endif
+
+// Suppress shadow and unused-value warning for sections:
+
+#if defined (__clang__)
+# define lest_SUPPRESS_WSHADOW    _Pragma( "clang diagnostic push" ) \
+                                  _Pragma( "clang diagnostic ignored \"-Wshadow\"" )
+# define lest_SUPPRESS_WUNUSED    _Pragma( "clang diagnostic push" ) \
+                                  _Pragma( "clang diagnostic ignored \"-Wunused-value\"" )
+# define lest_RESTORE_WARNINGS    _Pragma( "clang diagnostic pop"  )
+
+#elif defined (__GNUC__)
+# define lest_SUPPRESS_WSHADOW    _Pragma( "GCC diagnostic push" ) \
+                                  _Pragma( "GCC diagnostic ignored \"-Wshadow\"" )
+# define lest_SUPPRESS_WUNUSED    _Pragma( "GCC diagnostic push" ) \
+                                  _Pragma( "GCC diagnostic ignored \"-Wunused-value\"" )
+# define lest_RESTORE_WARNINGS    _Pragma( "GCC diagnostic pop"  )
+#else
+# define lest_SUPPRESS_WSHADOW    /*empty*/
+# define lest_SUPPRESS_WUNUSED    /*empty*/
+# define lest_RESTORE_WARNINGS    /*empty*/
+#endif
+
+// C++ language version detection (C++23 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef   lest_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+#  define lest_CPLUSPLUS  (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+#  define lest_CPLUSPLUS  __cplusplus
+# endif
+#endif
+
+#define lest_CPP98_OR_GREATER  ( lest_CPLUSPLUS >= 199711L )
+#define lest_CPP11_OR_GREATER  ( lest_CPLUSPLUS >= 201103L )
+#define lest_CPP14_OR_GREATER  ( lest_CPLUSPLUS >= 201402L )
+#define lest_CPP17_OR_GREATER  ( lest_CPLUSPLUS >= 201703L )
+#define lest_CPP20_OR_GREATER  ( lest_CPLUSPLUS >= 202002L )
+#define lest_CPP23_OR_GREATER  ( lest_CPLUSPLUS >= 202300L )
+
+#if ! defined( lest_NO_SHORT_MACRO_NAMES ) && ! defined( lest_NO_SHORT_ASSERTION_NAMES )
+# define MODULE            lest_MODULE
+
+# if ! lest_FEATURE_AUTO_REGISTER
+#  define CASE             lest_CASE
+#  define CASE_ON          lest_CASE_ON
+#  define SCENARIO         lest_SCENARIO
+# endif
+
+# define SETUP             lest_SETUP
+# define SECTION           lest_SECTION
+
+# define EXPECT            lest_EXPECT
+# define EXPECT_NOT        lest_EXPECT_NOT
+# define EXPECT_NO_THROW   lest_EXPECT_NO_THROW
+# define EXPECT_THROWS     lest_EXPECT_THROWS
+# define EXPECT_THROWS_AS  lest_EXPECT_THROWS_AS
+
+# define GIVEN             lest_GIVEN
+# define WHEN              lest_WHEN
+# define THEN              lest_THEN
+# define AND_WHEN          lest_AND_WHEN
+# define AND_THEN          lest_AND_THEN
+#endif
+
+#if lest_FEATURE_AUTO_REGISTER
+#define lest_SCENARIO( specification, sketch )  lest_CASE( specification, lest::text("Scenario: ") + sketch  )
+#else
+#define lest_SCENARIO( sketch  )  lest_CASE(    lest::text("Scenario: ") + sketch  )
+#endif
+#define lest_GIVEN(    context )  lest_SETUP(   lest::text("   Given: ") + context )
+#define lest_WHEN(     story   )  lest_SECTION( lest::text("    When: ") + story   )
+#define lest_THEN(     story   )  lest_SECTION( lest::text("    Then: ") + story   )
+#define lest_AND_WHEN( story   )  lest_SECTION( lest::text("And then: ") + story   )
+#define lest_AND_THEN( story   )  lest_SECTION( lest::text("And then: ") + story   )
+
+#if lest_FEATURE_AUTO_REGISTER
+
+# define lest_CASE( specification, proposition ) \
+    static void lest_FUNCTION( lest::env & ); \
+    namespace { lest::add_test lest_REGISTRAR( specification, lest::test( proposition, lest_FUNCTION ) ); } \
+    static void lest_FUNCTION( lest::env & lest_env )
+
+#else // lest_FEATURE_AUTO_REGISTER
+
+# define lest_CASE( proposition ) \
+    proposition, []( lest::env & lest_env )
+
+# define lest_CASE_ON( proposition, ... ) \
+    proposition, [__VA_ARGS__]( lest::env & lest_env )
+
+# define lest_MODULE( specification, module ) \
+    namespace { lest::add_module _( specification, module ); }
+
+#endif //lest_FEATURE_AUTO_REGISTER
+
+#define lest_SETUP( context ) \
+    for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \
+       for ( lest::ctx lest__ctx_setup( lest_env, context ); lest__ctx_setup; )
+
+#define lest_SECTION( proposition ) \
+    lest_SUPPRESS_WSHADOW \
+    static int lest_UNIQUE( id ) = 0; \
+    if ( lest::guard( lest_UNIQUE( id ), lest__section, lest__count ) ) \
+        for ( int lest__section = 0, lest__count = 1; lest__section < lest__count; lest__count -= 0==lest__section++ ) \
+            for ( lest::ctx lest__ctx_section( lest_env, proposition ); lest__ctx_section; ) \
+    lest_RESTORE_WARNINGS
+
+#define lest_EXPECT( expr ) \
+    do { \
+        try \
+        { \
+            if ( lest::result score = lest_DECOMPOSE( expr ) ) \
+                throw lest::failure{ lest_LOCATION, #expr, score.decomposition }; \
+            else if ( lest_env.pass() ) \
+                lest::report( lest_env.os, lest::passing{ lest_LOCATION, #expr, score.decomposition, lest_env.zen() }, lest_env.context() ); \
+        } \
+        catch(...) \
+        { \
+            lest::inform( lest_LOCATION, #expr ); \
+        } \
+    } while ( lest::is_false() )
+
+#define lest_EXPECT_NOT( expr ) \
+    do { \
+        try \
+        { \
+            if ( lest::result score = lest_DECOMPOSE( expr ) ) \
+            { \
+                if ( lest_env.pass() ) \
+                    lest::report( lest_env.os, lest::passing{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ), lest_env.zen() }, lest_env.context() ); \
+            } \
+            else \
+                throw lest::failure{ lest_LOCATION, lest::not_expr( #expr ), lest::not_expr( score.decomposition ) }; \
+        } \
+        catch(...) \
+        { \
+            lest::inform( lest_LOCATION, lest::not_expr( #expr ) ); \
+        } \
+    } while ( lest::is_false() )
+
+#define lest_EXPECT_NO_THROW( expr ) \
+    do \
+    { \
+        try \
+        { \
+            lest_SUPPRESS_WUNUSED \
+            expr; \
+            lest_RESTORE_WARNINGS \
+        } \
+        catch (...) \
+        { \
+            lest::inform( lest_LOCATION, #expr ); \
+        } \
+        if ( lest_env.pass() ) \
+            lest::report( lest_env.os, lest::got_none( lest_LOCATION, #expr ), lest_env.context() ); \
+    } while ( lest::is_false() )
+
+#define lest_EXPECT_THROWS( expr ) \
+    do \
+    { \
+        try \
+        { \
+            lest_SUPPRESS_WUNUSED \
+            expr; \
+            lest_RESTORE_WARNINGS \
+        } \
+        catch (...) \
+        { \
+            if ( lest_env.pass() ) \
+                lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr }, lest_env.context() ); \
+            break; \
+        } \
+        throw lest::expected{ lest_LOCATION, #expr }; \
+    } \
+    while ( lest::is_false() )
+
+#define lest_EXPECT_THROWS_AS( expr, excpt ) \
+    do \
+    { \
+        try \
+        { \
+            lest_SUPPRESS_WUNUSED \
+            expr; \
+            lest_RESTORE_WARNINGS \
+        }  \
+        catch ( excpt & ) \
+        { \
+            if ( lest_env.pass() ) \
+                lest::report( lest_env.os, lest::got{ lest_LOCATION, #expr, lest::of_type( #excpt ) }, lest_env.context() ); \
+            break; \
+        } \
+        catch (...) {} \
+        throw lest::expected{ lest_LOCATION, #expr, lest::of_type( #excpt ) }; \
+    } \
+    while ( lest::is_false() )
+
+#define lest_UNIQUE(  name       ) lest_UNIQUE2( name, __LINE__ )
+#define lest_UNIQUE2( name, line ) lest_UNIQUE3( name, line )
+#define lest_UNIQUE3( name, line ) name ## line
+
+#define lest_DECOMPOSE( expr ) ( lest::expression_decomposer() << expr )
+
+#define lest_FUNCTION  lest_UNIQUE(__lest_function__  )
+#define lest_REGISTRAR lest_UNIQUE(__lest_registrar__ )
+
+#define lest_LOCATION  lest::location{__FILE__, __LINE__}
+
+namespace lest {
+
+const int exit_max_value = 255;
+
+using text  = std::string;
+using texts = std::vector<text>;
+
+struct env;
+
+struct test
+{
+    text name;
+    std::function<void( env & )> behaviour;
+
+#if lest_FEATURE_AUTO_REGISTER
+    test( text name_, std::function<void( env & )> behaviour_ )
+    : name( name_), behaviour( behaviour_) {}
+#endif
+};
+
+using tests = std::vector<test>;
+
+#if lest_FEATURE_AUTO_REGISTER
+
+struct add_test
+{
+    add_test( tests & specification, test const & test_case )
+    {
+        specification.push_back( test_case );
+    }
+};
+
+#else
+
+struct add_module
+{
+    template< std::size_t N >
+    add_module( tests & specification, test const (&module)[N] )
+    {
+        specification.insert( specification.end(), std::begin( module ), std::end( module ) );
+    }
+};
+
+#endif
+
+struct result
+{
+    const bool passed;
+    const text decomposition;
+
+    template< typename T >
+    result( T const & passed_, text decomposition_)
+    : passed( !!passed_), decomposition( decomposition_) {}
+
+    explicit operator bool() { return ! passed; }
+};
+
+struct location
+{
+    const text file;
+    const int line;
+
+    location( text file_, int line_)
+    : file( file_), line( line_) {}
+};
+
+struct comment
+{
+    const text info;
+
+    comment( text info_) : info( info_) {}
+    explicit operator bool() { return ! info.empty(); }
+};
+
+struct message : std::runtime_error
+{
+    const text kind;
+    const location where;
+    const comment note;
+
+    ~message() throw() {}   // GCC 4.6
+
+    message( text kind_, location where_, text expr_, text note_ = "" )
+    : std::runtime_error( expr_), kind( kind_), where( where_), note( note_) {}
+};
+
+struct failure : message
+{
+    failure( location where_, text expr_, text decomposition_)
+    : message{ "failed", where_, expr_ + " for " + decomposition_ } {}
+};
+
+struct success : message
+{
+//    using message::message;   // VC is lagging here
+
+    success( text kind_, location where_, text expr_, text note_ = "" )
+    : message( kind_, where_, expr_, note_ ) {}
+};
+
+struct passing : success
+{
+    passing( location where_, text expr_, text decomposition_, bool zen )
+    : success( "passed", where_, expr_ + (zen ? "":" for " + decomposition_) ) {}
+};
+
+struct got_none : success
+{
+    got_none( location where_, text expr_ )
+    : success( "passed: got no exception", where_, expr_ ) {}
+};
+
+struct got : success
+{
+    got( location where_, text expr_)
+    : success( "passed: got exception", where_, expr_) {}
+
+    got( location where_, text expr_, text excpt_)
+    : success( "passed: got exception " + excpt_, where_, expr_) {}
+};
+
+struct expected : message
+{
+    expected( location where_, text expr_, text excpt_ = "" )
+    : message{ "failed: didn't get exception", where_, expr_, excpt_ } {}
+};
+
+struct unexpected : message
+{
+    unexpected( location where_, text expr_, text note_ = "" )
+    : message{ "failed: got unexpected exception", where_, expr_, note_ } {}
+};
+
+struct guard
+{
+    int & id;
+    int const & section;
+
+    guard( int & id_, int const & section_, int & count )
+    : id( id_), section( section_)
+    {
+        if ( section == 0 )
+            id = count++ - 1;
+    }
+    operator bool() { return id == section; }
+};
+
+class approx
+{
+public:
+    explicit approx ( double magnitude )
+    : epsilon_  { std::numeric_limits<float>::epsilon() * 100 }
+    , scale_    { 1.0 }
+    , magnitude_{ magnitude } {}
+
+    approx( approx const & other ) = default;
+
+    static approx custom() { return approx( 0 ); }
+
+    approx operator()( double new_magnitude )
+    {
+        approx appr( new_magnitude );
+        appr.epsilon( epsilon_ );
+        appr.scale  ( scale_   );
+        return appr;
+    }
+
+    double magnitude() const { return magnitude_; }
+
+    approx & epsilon( double epsilon ) { epsilon_ = epsilon; return *this; }
+    approx & scale  ( double scale   ) { scale_   = scale;   return *this; }
+
+    friend bool operator == ( double lhs, approx const & rhs )
+    {
+        // Thanks to Richard Harris for his help refining this formula.
+        return std::abs( lhs - rhs.magnitude_ ) < rhs.epsilon_ * ( rhs.scale_ + (std::min)( std::abs( lhs ), std::abs( rhs.magnitude_ ) ) );
+    }
+
+    friend bool operator == ( approx const & lhs, double rhs ) { return  operator==( rhs, lhs ); }
+    friend bool operator != ( double lhs, approx const & rhs ) { return !operator==( lhs, rhs ); }
+    friend bool operator != ( approx const & lhs, double rhs ) { return !operator==( rhs, lhs ); }
+
+    friend bool operator <= ( double lhs, approx const & rhs ) { return lhs < rhs.magnitude_ || lhs == rhs; }
+    friend bool operator <= ( approx const & lhs, double rhs ) { return lhs.magnitude_ < rhs || lhs == rhs; }
+    friend bool operator >= ( double lhs, approx const & rhs ) { return lhs > rhs.magnitude_ || lhs == rhs; }
+    friend bool operator >= ( approx const & lhs, double rhs ) { return lhs.magnitude_ > rhs || lhs == rhs; }
+
+private:
+    double epsilon_;
+    double scale_;
+    double magnitude_;
+};
+
+inline bool is_false(           ) { return false; }
+inline bool is_true ( bool flag ) { return  flag; }
+
+inline text not_expr( text message )
+{
+    return "! ( " + message + " )";
+}
+
+inline text with_message( text message )
+{
+    return "with message \"" + message + "\"";
+}
+
+inline text of_type( text type )
+{
+    return "of type " + type;
+}
+
+inline void inform( location where, text expr )
+{
+    try
+    {
+        throw;
+    }
+    catch( message const & )
+    {
+        throw;
+    }
+    catch( std::exception const & e )
+    {
+        throw unexpected{ where, expr, with_message( e.what() ) }; \
+    }
+    catch(...)
+    {
+        throw unexpected{ where, expr, "of unknown type" }; \
+    }
+}
+
+// Expression decomposition:
+
+template< typename T >
+auto make_value_string( T const & value ) -> std::string;
+
+template< typename T >
+auto make_memory_string( T const & item ) -> std::string;
+
+#if lest_FEATURE_LITERAL_SUFFIX
+inline char const * sfx( char const  * txt ) { return txt; }
+#else
+inline char const * sfx( char const  *      ) { return ""; }
+#endif
+
+inline std::string transformed( char chr )
+{
+    struct Tr { char chr; char const * str; } table[] =
+    {
+        {'\\', "\\\\" },
+        {'\r', "\\r"  }, {'\f', "\\f" },
+        {'\n', "\\n"  }, {'\t', "\\t" },
+    };
+
+    for ( auto tr : table )
+    {
+        if ( chr == tr.chr )
+            return tr.str;
+    }
+
+    auto unprintable = [](char c){ return 0 <= c && c < ' '; };
+
+    auto to_hex_string = [](char c)
+    {
+        std::ostringstream os;
+        os << "\\x" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>( static_cast<unsigned char>(c) );
+        return os.str();
+    };
+
+    return unprintable( chr  ) ? to_hex_string( chr ) : std::string( 1, chr );
+}
+
+inline std::string make_tran_string( std::string const & txt ) { std::ostringstream os; for(auto c:txt) os << transformed(c); return os.str(); }
+inline std::string make_strg_string( std::string const & txt ) { return "\"" + make_tran_string(                 txt   ) + "\"" ; }
+inline std::string make_char_string(                char chr ) { return "\'" + make_tran_string( std::string( 1, chr ) ) + "\'" ; }
+
+inline std::string to_string( std::nullptr_t              ) { return "nullptr"; }
+inline std::string to_string( std::string     const & txt ) { return make_strg_string( txt ); }
+#if lest_FEATURE_WSTRING
+inline std::string to_string( std::wstring    const & txt ) ;
+#endif
+
+inline std::string to_string( char    const * const   txt ) { return txt ? make_strg_string( txt ) : "{null string}"; }
+inline std::string to_string( char          * const   txt ) { return txt ? make_strg_string( txt ) : "{null string}"; }
+#if lest_FEATURE_WSTRING
+inline std::string to_string( wchar_t const * const   txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; }
+inline std::string to_string( wchar_t       * const   txt ) { return txt ? to_string( std::wstring( txt ) ) : "{null string}"; }
+#endif
+
+inline std::string to_string(          bool          flag ) { return flag ? "true" : "false"; }
+
+inline std::string to_string(   signed short        value ) { return make_value_string( value ) ;             }
+inline std::string to_string( unsigned short        value ) { return make_value_string( value ) + sfx("u"  ); }
+inline std::string to_string(   signed   int        value ) { return make_value_string( value ) ;             }
+inline std::string to_string( unsigned   int        value ) { return make_value_string( value ) + sfx("u"  ); }
+inline std::string to_string(   signed  long        value ) { return make_value_string( value ) + sfx("l"  ); }
+inline std::string to_string( unsigned  long        value ) { return make_value_string( value ) + sfx("ul" ); }
+inline std::string to_string(   signed  long long   value ) { return make_value_string( value ) + sfx("ll" ); }
+inline std::string to_string( unsigned  long long   value ) { return make_value_string( value ) + sfx("ull"); }
+inline std::string to_string(         double        value ) { return make_value_string( value ) ;             }
+inline std::string to_string(          float        value ) { return make_value_string( value ) + sfx("f"  ); }
+
+inline std::string to_string(   signed char           chr ) { return make_char_string( static_cast<char>( chr ) ); }
+inline std::string to_string( unsigned char           chr ) { return make_char_string( static_cast<char>( chr ) ); }
+inline std::string to_string(          char           chr ) { return make_char_string(                    chr   ); }
+
+template< typename T >
+struct is_streamable
+{
+    template< typename U >
+    static auto test( int ) -> decltype( std::declval<std::ostream &>() << std::declval<U>(), std::true_type() );
+
+    template< typename >
+    static auto test( ... ) -> std::false_type;
+
+#ifdef _MSC_VER
+    enum { value = std::is_same< decltype( test<T>(0) ), std::true_type >::value };
+#else
+    static constexpr bool value = std::is_same< decltype( test<T>(0) ), std::true_type >::value;
+#endif
+};
+
+template< typename T >
+struct is_container
+{
+    template< typename U >
+    static auto test( int ) -> decltype( std::declval<U>().begin() == std::declval<U>().end(), std::true_type() );
+
+    template< typename >
+    static auto test( ... ) -> std::false_type;
+
+#ifdef _MSC_VER
+    enum { value = std::is_same< decltype( test<T>(0) ), std::true_type >::value };
+#else
+    static constexpr bool value = std::is_same< decltype( test<T>(0) ), std::true_type >::value;
+#endif
+};
+
+template< typename T, typename R >
+using ForEnum = typename std::enable_if< std::is_enum<T>::value, R>::type;
+
+template< typename T, typename R >
+using ForNonEnum = typename std::enable_if< ! std::is_enum<T>::value, R>::type;
+
+template< typename T, typename R >
+using ForStreamable = typename std::enable_if< is_streamable<T>::value, R>::type;
+
+template< typename T, typename R >
+using ForNonStreamable = typename std::enable_if< ! is_streamable<T>::value, R>::type;
+
+template< typename T, typename R >
+using ForContainer = typename std::enable_if< is_container<T>::value, R>::type;
+
+template< typename T, typename R >
+using ForNonContainerNonPointer = typename std::enable_if< ! (is_container<T>::value || std::is_pointer<T>::value), R>::type;
+
+template< typename T >
+auto make_enum_string( T const & item ) -> ForNonEnum<T, std::string>
+{
+#if lest__cpp_rtti
+    return text("[type: ") + typeid(T).name() + "]: " + make_memory_string( item );
+#else
+    return text("[type: (no RTTI)]: ") + make_memory_string( item );
+#endif
+}
+
+template< typename T >
+auto make_enum_string( T const & item ) -> ForEnum<T, std::string>
+{
+    return to_string( static_cast<typename std::underlying_type<T>::type>( item ) );
+}
+
+template< typename T >
+auto make_string( T const & item ) -> ForNonStreamable<T, std::string>
+{
+    return make_enum_string( item );
+}
+
+template< typename T >
+auto make_string( T const & item ) -> ForStreamable<T, std::string>
+{
+    std::ostringstream os; os << item; return os.str();
+}
+
+template<typename T1, typename T2>
+auto make_string( std::pair<T1,T2> const & pair ) -> std::string
+{
+    std::ostringstream oss;
+    oss << "{ " << to_string( pair.first ) << ", " << to_string( pair.second ) << " }";
+    return oss.str();
+}
+
+template< typename TU, std::size_t N >
+struct make_tuple_string
+{
+    static std::string make( TU const & tuple )
+    {
+        std::ostringstream os;
+        os << to_string( std::get<N - 1>( tuple ) ) << ( N < std::tuple_size<TU>::value ? ", ": " ");
+        return make_tuple_string<TU, N - 1>::make( tuple ) + os.str();
+    }
+};
+
+template< typename TU >
+struct make_tuple_string<TU, 0>
+{
+    static std::string make( TU const & ) { return ""; }
+};
+
+template< typename ...TS >
+auto make_string( std::tuple<TS...> const & tuple ) -> std::string
+{
+    return "{ " + make_tuple_string<std::tuple<TS...>, sizeof...(TS)>::make( tuple ) + "}";
+}
+
+template< typename T >
+inline std::string make_string( T const * ptr )
+{
+    // Note showbase affects the behavior of /integer/ output;
+    std::ostringstream os;
+    os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(T*) ) << std::setfill('0') << reinterpret_cast<std::ptrdiff_t>( ptr );
+    return os.str();
+}
+
+template< typename C, typename R >
+inline std::string make_string( R C::* ptr )
+{
+    std::ostringstream os;
+    os << std::internal << std::hex << std::showbase << std::setw( 2 + 2 * sizeof(R C::* ) ) << std::setfill('0') << ptr;
+    return os.str();
+}
+
+template< typename T >
+auto to_string( T const * ptr ) -> std::string
+{
+    return ! ptr ? "nullptr" : make_string( ptr );
+}
+
+template<typename C, typename R>
+auto to_string( R C::* ptr ) -> std::string
+{
+    return ! ptr ? "nullptr" : make_string( ptr );
+}
+
+template< typename T >
+auto to_string( T const & item ) -> ForNonContainerNonPointer<T, std::string>
+{
+    return make_string( item );
+}
+
+template< typename C >
+auto to_string( C const & cont ) -> ForContainer<C, std::string>
+{
+    std::ostringstream os;
+    os << "{ ";
+    for ( auto & x : cont )
+    {
+        os << to_string( x ) << ", ";
+    }
+    os << "}";
+    return os.str();
+}
+
+#if lest_FEATURE_WSTRING
+inline
+auto to_string( std::wstring const & txt ) -> std::string
+{
+    std::string result; result.reserve( txt.size() );
+
+    for( auto & chr : txt )
+    {
+        result += chr <= 0xff ? static_cast<char>( chr ) : '?';
+    }
+    return to_string( result );
+}
+#endif
+
+template< typename T >
+auto make_value_string( T const & value ) -> std::string
+{
+    std::ostringstream os; os << value; return os.str();
+}
+
+inline
+auto make_memory_string( void const * item, std::size_t size ) -> std::string
+{
+    // reverse order for little endian architectures:
+
+    auto is_little_endian = []
+    {
+        union U { int i = 1; char c[ sizeof(int) ]; };
+
+        return 1 != U{}.c[ sizeof(int) - 1 ];
+    };
+
+    int i = 0, end = static_cast<int>( size ), inc = 1;
+
+    if ( is_little_endian() ) { i = end - 1; end = inc = -1; }
+
+    unsigned char const * bytes = static_cast<unsigned char const *>( item );
+
+    std::ostringstream os;
+    os << "0x" << std::setfill( '0' ) << std::hex;
+    for ( ; i != end; i += inc )
+    {
+        os << std::setw(2) << static_cast<unsigned>( bytes[i] ) << " ";
+    }
+    return os.str();
+}
+
+template< typename T >
+auto make_memory_string( T const & item ) -> std::string
+{
+    return make_memory_string( &item, sizeof item );
+}
+
+inline
+auto to_string( approx const & appr ) -> std::string
+{
+    return to_string( appr.magnitude() );
+}
+
+template< typename L, typename R >
+auto to_string( L const & lhs, std::string op, R const & rhs ) -> std::string
+{
+    std::ostringstream os; os << to_string( lhs ) << " " << op << " " << to_string( rhs ); return os.str();
+}
+
+template< typename L >
+struct expression_lhs
+{
+    const L lhs;
+
+    expression_lhs( L lhs_) : lhs( lhs_) {}
+
+    operator result() { return result{ !!lhs, to_string( lhs ) }; }
+
+    template< typename R > result operator==( R const & rhs ) { return result{ lhs == rhs, to_string( lhs, "==", rhs ) }; }
+    template< typename R > result operator!=( R const & rhs ) { return result{ lhs != rhs, to_string( lhs, "!=", rhs ) }; }
+    template< typename R > result operator< ( R const & rhs ) { return result{ lhs <  rhs, to_string( lhs, "<" , rhs ) }; }
+    template< typename R > result operator<=( R const & rhs ) { return result{ lhs <= rhs, to_string( lhs, "<=", rhs ) }; }
+    template< typename R > result operator> ( R const & rhs ) { return result{ lhs >  rhs, to_string( lhs, ">" , rhs ) }; }
+    template< typename R > result operator>=( R const & rhs ) { return result{ lhs >= rhs, to_string( lhs, ">=", rhs ) }; }
+};
+
+struct expression_decomposer
+{
+    template <typename L>
+    expression_lhs<L const &> operator<< ( L const & operand )
+    {
+        return expression_lhs<L const &>( operand );
+    }
+};
+
+// Reporter:
+
+#if lest_FEATURE_COLOURISE
+
+inline text red  ( text words ) { return "\033[1;31m" + words + "\033[0m"; }
+inline text green( text words ) { return "\033[1;32m" + words + "\033[0m"; }
+inline text gray ( text words ) { return "\033[1;30m" + words + "\033[0m"; }
+
+inline bool starts_with( text words, text with )
+{
+    return 0 == words.find( with );
+}
+
+inline text replace( text words, text from, text to )
+{
+    size_t pos = words.find( from );
+    return pos == std::string::npos ? words : words.replace( pos, from.length(), to  );
+}
+
+inline text colour( text words )
+{
+    if      ( starts_with( words, "failed" ) ) return replace( words, "failed", red  ( "failed" ) );
+    else if ( starts_with( words, "passed" ) ) return replace( words, "passed", green( "passed" ) );
+
+    return replace( words, "for", gray( "for" ) );
+}
+
+inline bool is_cout( std::ostream & os ) { return &os == &std::cout; }
+
+struct colourise
+{
+    const text words;
+
+    colourise( text words )
+    : words( words ) {}
+
+    // only colourise for std::cout, not for a stringstream as used in tests:
+
+    std::ostream & operator()( std::ostream & os ) const
+    {
+        return is_cout( os ) ? os << colour( words ) : os << words;
+    }
+};
+
+inline std::ostream & operator<<( std::ostream & os, colourise words ) { return words( os ); }
+#else
+inline text colourise( text words ) { return words; }
+#endif
+
+inline text pluralise( text word, int n )
+{
+    return n == 1 ? word : word + "s";
+}
+
+inline std::ostream & operator<<( std::ostream & os, comment note )
+{
+    return os << (note ? " " + note.info : "" );
+}
+
+inline std::ostream & operator<<( std::ostream & os, location where )
+{
+#ifdef __GNUG__
+    return os << where.file << ":" << where.line;
+#else
+    return os << where.file << "(" << where.line << ")";
+#endif
+}
+
+inline void report( std::ostream & os, message const & e, text test )
+{
+    os << e.where << ": " << colourise( e.kind ) << e.note << ": " << test << ": " << colourise( e.what() ) << std::endl;
+}
+
+// Test runner:
+
+#if lest_FEATURE_REGEX_SEARCH
+    inline bool search( text re, text line )
+    {
+        return std::regex_search( line, std::regex( re ) );
+    }
+#else
+    inline bool search( text part, text line )
+    {
+        auto case_insensitive_equal = []( char a, char b )
+        {
+            return tolower( a ) == tolower( b );
+        };
+
+        return std::search(
+            line.begin(), line.end(),
+            part.begin(), part.end(), case_insensitive_equal ) != line.end();
+    }
+#endif
+
+inline bool match( texts whats, text line )
+{
+    for ( auto & what : whats )
+    {
+        if ( search( what, line ) )
+            return true;
+    }
+    return false;
+}
+
+inline bool select( text name, texts include )
+{
+    auto none = []( texts args ) { return args.size() == 0; };
+
+#if lest_FEATURE_REGEX_SEARCH
+    auto hidden = []( text arg ){ return match( { "\\[\\..*", "\\[hide\\]" }, arg ); };
+#else
+    auto hidden = []( text arg ){ return match( { "[.", "[hide]" }, arg ); };
+#endif
+
+    if ( none( include ) )
+    {
+        return ! hidden( name );
+    }
+
+    bool any = false;
+    for ( auto pos = include.rbegin(); pos != include.rend(); ++pos )
+    {
+        auto & part = *pos;
+
+        if ( part == "@" || part == "*" )
+            return true;
+
+        if ( search( part, name ) )
+            return true;
+
+        if ( '!' == part[0] )
+        {
+            any = true;
+            if ( search( part.substr(1), name ) )
+                return false;
+        }
+        else
+        {
+            any = false;
+        }
+    }
+    return any && ! hidden( name );
+}
+
+inline int indefinite( int repeat ) { return repeat == -1; }
+
+using seed_t = std::mt19937::result_type;
+
+struct options
+{
+    bool help    = false;
+    bool abort   = false;
+    bool count   = false;
+    bool list    = false;
+    bool tags    = false;
+    bool time    = false;
+    bool pass    = false;
+    bool zen     = false;
+    bool lexical = false;
+    bool random  = false;
+    bool verbose = false;
+    bool version = false;
+    int  repeat  = 1;
+    seed_t seed  = 0;
+};
+
+struct env
+{
+    std::ostream & os;
+    options opt;
+    text testing;
+    std::vector< text > ctx;
+
+    env( std::ostream & out, options option )
+    : os( out ), opt( option ), testing(), ctx() {}
+
+    env & operator()( text test )
+    {
+        clear(); testing = test; return *this;
+    }
+
+    bool abort() { return opt.abort; }
+    bool pass()  { return opt.pass; }
+    bool zen()   { return opt.zen; }
+
+    void clear() { ctx.clear(); }
+    void pop()   { ctx.pop_back(); }
+    void push( text proposition ) { ctx.emplace_back( proposition ); }
+
+    text context() { return testing + sections(); }
+
+    text sections()
+    {
+        if ( ! opt.verbose )
+            return "";
+
+        text msg;
+        for( auto section : ctx )
+        {
+            msg += "\n  " + section;
+        }
+        return msg;
+    }
+};
+
+struct ctx
+{
+    env & environment;
+    bool once;
+
+    ctx( env & environment_, text proposition_ )
+    : environment( environment_), once( true )
+    {
+        environment.push( proposition_);
+    }
+
+    ~ctx()
+    {
+#if lest_CPP17_OR_GREATER
+        if ( std::uncaught_exceptions() == 0 )
+#else
+        if ( ! std::uncaught_exception() )
+#endif
+        {
+            environment.pop();
+        }
+    }
+
+    explicit operator bool() { bool result = once; once = false; return result; }
+};
+
+struct action
+{
+    std::ostream & os;
+
+    action( std::ostream & out ) : os( out ) {}
+
+    action( action const & ) = delete;
+    void operator=( action const & ) = delete;
+
+    operator      int() { return 0; }
+    bool        abort() { return false; }
+    action & operator()( test ) { return *this; }
+};
+
+struct print : action
+{
+    print( std::ostream & out ) : action( out ) {}
+
+    print & operator()( test testing )
+    {
+        os << testing.name << "\n"; return *this;
+    }
+};
+
+inline texts tags( text name, texts result = {} )
+{
+    auto none = std::string::npos;
+    auto lb   = name.find_first_of( "[" );
+    auto rb   = name.find_first_of( "]" );
+
+    if ( lb == none || rb == none )
+        return result;
+
+    result.emplace_back( name.substr( lb, rb - lb + 1 ) );
+
+    return tags( name.substr( rb + 1 ), result );
+}
+
+struct ptags : action
+{
+    std::set<text> result;
+
+    ptags( std::ostream & out ) : action( out ), result() {}
+
+    ptags & operator()( test testing )
+    {
+        for ( auto & tag : tags( testing.name ) )
+            result.insert( tag );
+
+        return *this;
+    }
+
+    ~ptags()
+    {
+        std::copy( result.begin(), result.end(), std::ostream_iterator<text>( os, "\n" ) );
+    }
+};
+
+struct count : action
+{
+    int n = 0;
+
+    count( std::ostream & out ) : action( out ) {}
+
+    count & operator()( test ) { ++n; return *this; }
+
+    ~count()
+    {
+        os << n << " selected " << pluralise("test", n) << "\n";
+    }
+};
+
+struct timer
+{
+    using time = std::chrono::high_resolution_clock;
+
+    time::time_point start = time::now();
+
+    double elapsed_seconds() const
+    {
+        return 1e-6 * static_cast<double>( std::chrono::duration_cast< std::chrono::microseconds >( time::now() - start ).count() );
+    }
+};
+
+struct times : action
+{
+    env output;
+    int selected = 0;
+    int failures = 0;
+
+    timer total;
+
+    times( std::ostream & out, options option )
+    : action( out ), output( out, option ), total()
+    {
+        os << std::setfill(' ') << std::fixed << std::setprecision( lest_FEATURE_TIME_PRECISION );
+    }
+
+    operator int() { return failures; }
+
+    bool abort() { return output.abort() && failures > 0; }
+
+    times & operator()( test testing )
+    {
+        timer t;
+
+        try
+        {
+            testing.behaviour( output( testing.name ) );
+        }
+        catch( message const & )
+        {
+            ++failures;
+        }
+
+        os << std::setw(3) << ( 1000 * t.elapsed_seconds() ) << " ms: " << testing.name  << "\n";
+
+        return *this;
+    }
+
+    ~times()
+    {
+        os << "Elapsed time: " << std::setprecision(1) << total.elapsed_seconds() << " s\n";
+    }
+};
+
+struct confirm : action
+{
+    env output;
+    int selected = 0;
+    int failures = 0;
+
+    confirm( std::ostream & out, options option )
+    : action( out ), output( out, option ) {}
+
+    operator int() { return failures; }
+
+    bool abort() { return output.abort() && failures > 0; }
+
+    confirm & operator()( test testing )
+    {
+        try
+        {
+            ++selected; testing.behaviour( output( testing.name ) );
+        }
+        catch( message const & e )
+        {
+            ++failures; report( os, e, output.context() );
+        }
+        return *this;
+    }
+
+    ~confirm()
+    {
+        if ( failures > 0 )
+        {
+            os << failures << " out of " << selected << " selected " << pluralise("test", selected) << " " << colourise( "failed.\n" );
+        }
+        else if ( output.pass() )
+        {
+            os << "All " << selected << " selected " << pluralise("test", selected) << " " << colourise( "passed.\n" );
+        }
+    }
+};
+
+template< typename Action >
+bool abort( Action & perform )
+{
+    return perform.abort();
+}
+
+template< typename Action >
+Action && for_test( tests specification, texts in, Action && perform, int n = 1 )
+{
+    for ( int i = 0; indefinite( n ) || i < n; ++i )
+    {
+        for ( auto & testing : specification )
+        {
+            if ( select( testing.name, in ) )
+                if ( abort( perform( testing ) ) )
+                    return std::move( perform );
+        }
+    }
+    return std::move( perform );
+}
+
+inline void sort( tests & specification )
+{
+    auto test_less = []( test const & a, test const & b ) { return a.name < b.name; };
+    std::sort( specification.begin(), specification.end(), test_less );
+}
+
+inline void shuffle( tests & specification, options option )
+{
+    std::shuffle( specification.begin(), specification.end(), std::mt19937( option.seed ) );
+}
+
+// workaround MinGW bug, http://stackoverflow.com/a/16132279:
+
+inline int stoi( text num )
+{
+    return static_cast<int>( std::strtol( num.c_str(), nullptr, 10 ) );
+}
+
+inline bool is_number( text arg )
+{
+    return std::all_of( arg.begin(), arg.end(), ::isdigit );
+}
+
+inline seed_t seed( text opt, text arg )
+{
+    if ( is_number( arg ) )
+        return static_cast<seed_t>( lest::stoi( arg ) );
+
+    if ( arg == "time" )
+        return static_cast<seed_t>( std::chrono::high_resolution_clock::now().time_since_epoch().count() );
+
+    throw std::runtime_error( "expecting 'time' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" );
+}
+
+inline int repeat( text opt, text arg )
+{
+    const int num = lest::stoi( arg );
+
+    if ( indefinite( num ) || num >= 0 )
+        return num;
+
+    throw std::runtime_error( "expecting '-1' or positive number with option '" + opt + "', got '" + arg + "' (try option --help)" );
+}
+
+inline auto split_option( text arg ) -> std::tuple<text, text>
+{
+    auto pos = arg.rfind( '=' );
+
+    return pos == text::npos
+                ? std::make_tuple( arg, "" )
+                : std::make_tuple( arg.substr( 0, pos ), arg.substr( pos + 1 ) );
+}
+
+inline auto split_arguments( texts args ) -> std::tuple<options, texts>
+{
+    options option; texts in;
+
+    bool in_options = true;
+
+    for ( auto & arg : args )
+    {
+        if ( in_options )
+        {
+            text opt, val;
+            std::tie( opt, val ) = split_option( arg );
+
+            if      ( opt[0] != '-'                             ) { in_options     = false;           }
+            else if ( opt == "--"                               ) { in_options     = false; continue; }
+            else if ( opt == "-h"      || "--help"       == opt ) { option.help    =  true; continue; }
+            else if ( opt == "-a"      || "--abort"      == opt ) { option.abort   =  true; continue; }
+            else if ( opt == "-c"      || "--count"      == opt ) { option.count   =  true; continue; }
+            else if ( opt == "-g"      || "--list-tags"  == opt ) { option.tags    =  true; continue; }
+            else if ( opt == "-l"      || "--list-tests" == opt ) { option.list    =  true; continue; }
+            else if ( opt == "-t"      || "--time"       == opt ) { option.time    =  true; continue; }
+            else if ( opt == "-p"      || "--pass"       == opt ) { option.pass    =  true; continue; }
+            else if ( opt == "-z"      || "--pass-zen"   == opt ) { option.zen     =  true; continue; }
+            else if ( opt == "-v"      || "--verbose"    == opt ) { option.verbose =  true; continue; }
+            else if (                     "--version"    == opt ) { option.version =  true; continue; }
+            else if ( opt == "--order" && "declared"     == val ) { /* by definition */   ; continue; }
+            else if ( opt == "--order" && "lexical"      == val ) { option.lexical =  true; continue; }
+            else if ( opt == "--order" && "random"       == val ) { option.random  =  true; continue; }
+            else if ( opt == "--random-seed" ) { option.seed   = seed  ( "--random-seed", val ); continue; }
+            else if ( opt == "--repeat"      ) { option.repeat = repeat( "--repeat"     , val ); continue; }
+            else throw std::runtime_error( "unrecognised option '" + arg + "' (try option --help)" );
+        }
+        in.push_back( arg );
+    }
+    option.pass = option.pass || option.zen;
+
+    return std::make_tuple( option, in );
+}
+
+inline int usage( std::ostream & os )
+{
+    os <<
+        "\nUsage: test [options] [test-spec ...]\n"
+        "\n"
+        "Options:\n"
+        "  -h, --help         this help message\n"
+        "  -a, --abort        abort at first failure\n"
+        "  -c, --count        count selected tests\n"
+        "  -g, --list-tags    list tags of selected tests\n"
+        "  -l, --list-tests   list selected tests\n"
+        "  -p, --pass         also report passing tests\n"
+        "  -z, --pass-zen     ... without expansion\n"
+        "  -t, --time         list duration of selected tests\n"
+        "  -v, --verbose      also report passing or failing sections\n"
+        "  --order=declared   use source code test order (default)\n"
+        "  --order=lexical    use lexical sort test order\n"
+        "  --order=random     use random test order\n"
+        "  --random-seed=n    use n for random generator seed\n"
+        "  --random-seed=time use time for random generator seed\n"
+        "  --repeat=n         repeat selected tests n times (-1: indefinite)\n"
+        "  --version          report lest version and compiler used\n"
+        "  --                 end options\n"
+        "\n"
+        "Test specification:\n"
+        "  \"@\", \"*\" all tests, unless excluded\n"
+        "  empty    all tests, unless tagged [hide] or [.optional-name]\n"
+#if lest_FEATURE_REGEX_SEARCH
+        "  \"re\"     select tests that match regular expression\n"
+        "  \"!re\"    omit tests that match regular expression\n"
+#else
+        "  \"text\"   select tests that contain text (case insensitive)\n"
+        "  \"!text\"  omit tests that contain text (case insensitive)\n"
+#endif
+        ;
+    return 0;
+}
+
+inline text compiler()
+{
+    std::ostringstream os;
+#if   defined (__clang__ )
+    os << "clang " << __clang_version__;
+#elif defined (__GNUC__  )
+    os << "gcc " << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__;
+#elif defined ( _MSC_VER )
+    os << "MSVC " << (_MSC_VER / 100 - 5 - (_MSC_VER < 1900)) << " (" << _MSC_VER << ")";
+#else
+    os << "[compiler]";
+#endif
+    return os.str();
+}
+
+inline int version( std::ostream & os )
+{
+    os << "lest version "  << lest_VERSION << "\n"
+       << "Compiled with " << compiler()   << " on " << __DATE__ << " at " << __TIME__ << ".\n"
+       << "For more information, see https://github.com/martinmoene/lest.\n";
+    return 0;
+}
+
+inline int run( tests specification, texts arguments, std::ostream & os = std::cout )
+{
+    try
+    {
+        options option; texts in;
+        std::tie( option, in ) = split_arguments( arguments );
+
+        if ( option.lexical ) {    sort( specification         ); }
+        if ( option.random  ) { shuffle( specification, option ); }
+
+        if ( option.help    ) { return usage   ( os ); }
+        if ( option.version ) { return version ( os ); }
+        if ( option.count   ) { return for_test( specification, in, count( os ) ); }
+        if ( option.list    ) { return for_test( specification, in, print( os ) ); }
+        if ( option.tags    ) { return for_test( specification, in, ptags( os ) ); }
+        if ( option.time    ) { return for_test( specification, in, times( os, option ) ); }
+
+        return for_test( specification, in, confirm( os, option ), option.repeat );
+    }
+    catch ( std::exception const & e )
+    {
+        os << "Error: " << e.what() << "\n";
+        return 1;
+    }
+}
+
+inline int run( tests specification, int argc, char * argv[], std::ostream & os = std::cout )
+{
+    return run( specification, texts( argv + 1, argv + argc ), os  );
+}
+
+template< std::size_t N >
+int run( test const (&specification)[N], texts arguments, std::ostream & os = std::cout )
+{
+    std::cout.sync_with_stdio( false );
+    return (std::min)( run( tests( specification, specification + N ), arguments, os  ), exit_max_value );
+}
+
+template< std::size_t N >
+int run( test const (&specification)[N], std::ostream & os = std::cout )
+{
+    return run( tests( specification, specification + N ), {}, os  );
+}
+
+template< std::size_t N >
+int run( test const (&specification)[N], int argc, char * argv[], std::ostream & os = std::cout )
+{
+    return run( tests( specification, specification + N ), texts( argv + 1, argv + argc ), os  );
+}
+
+} // namespace lest
+
+#if defined (__clang__)
+# pragma clang diagnostic pop
+#elif defined (__GNUC__)
+# pragma GCC   diagnostic pop
+#endif
+
+#endif // LEST_LEST_HPP_INCLUDED
diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp
new file mode 100644
index 0000000..ecedc70
--- /dev/null
+++ b/tests/unit/main.cpp
@@ -0,0 +1,33 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "framework.test.h"
+
+int main(int argc, char* argv[])
+{
+  return lest::run(lws_test::get_tests(), argc, argv);
+}
diff --git a/tests/unit/wire/CMakeLists.txt b/tests/unit/wire/CMakeLists.txt
new file mode 100644
index 0000000..f04d781
--- /dev/null
+++ b/tests/unit/wire/CMakeLists.txt
@@ -0,0 +1,39 @@
+# Copyright (c) 2022, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+add_subdirectory(json)
+
+add_library(monero-lws-unit-wire OBJECT read.write.test.cpp read.test.cpp)
+target_link_libraries(
+  monero-lws-unit-wire
+  monero-lws-unit-framework
+  monero-lws-wire
+  monero::libraries
+)
+#add_test(monero-lws-unit)
diff --git a/tests/unit/wire/base.test.h b/tests/unit/wire/base.test.h
new file mode 100644
index 0000000..46be987
--- /dev/null
+++ b/tests/unit/wire/base.test.h
@@ -0,0 +1,54 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <cstdint>
+#include <cstring>
+#include <type_traits>
+#include "wire/traits.h"
+
+namespace lws_test
+{
+  struct small_blob { std::uint8_t buf[4]; };
+  constexpr const small_blob blob_test1{{0x00, 0xFF, 0x22, 0x11}};
+  constexpr const small_blob blob_test2{{0x11, 0x7F, 0x7E, 0x80}};
+  constexpr const small_blob blob_test3{{0xDE, 0xAD, 0xBE, 0xEF}};
+
+  inline bool operator==(const small_blob& lhs, const small_blob& rhs)
+  {
+    return std::memcmp(lhs.buf, rhs.buf, sizeof(lhs.buf)) == 0;
+  }
+}
+
+namespace wire
+{
+  template<>
+  struct is_blob<lws_test::small_blob>
+    : std::true_type
+  {};
+}
diff --git a/tests/unit/wire/json/CMakeLists.txt b/tests/unit/wire/json/CMakeLists.txt
new file mode 100644
index 0000000..50a497e
--- /dev/null
+++ b/tests/unit/wire/json/CMakeLists.txt
@@ -0,0 +1,37 @@
+# Copyright (c) 2022, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+add_library(monero-lws-unit-wire-json OBJECT read.write.test.cpp)
+target_link_libraries(
+  monero-lws-unit-wire-json
+  monero-lws-unit-framework
+  monero-lws-wire-json
+  monero::libraries
+)
+
diff --git a/tests/unit/wire/json/read.write.test.cpp b/tests/unit/wire/json/read.write.test.cpp
new file mode 100644
index 0000000..47aab43
--- /dev/null
+++ b/tests/unit/wire/json/read.write.test.cpp
@@ -0,0 +1,109 @@
+
+#include "framework.test.h"
+
+#include <boost/core/demangle.hpp>
+#include <boost/range/algorithm/equal.hpp>
+#include <cstdint>
+#include <type_traits>
+#include "wire/traits.h"
+#include "wire/json/base.h"
+#include "wire/json/read.h"
+#include "wire/json/write.h"
+#include "wire/vector.h"
+
+#include "wire/base.test.h"
+
+namespace
+{
+  constexpr const char basic_string[] = u8"my_string_data";
+  constexpr const char basic_json[] =
+    u8"{\"utf8\":\"my_string_data\",\"vec\":[0,127],\"data\":\"00ff2211\",\"choice\":true}";
+
+  template<typename T>
+  struct basic_object
+  {
+    std::string utf8;
+    std::vector<T> vec;
+    lws_test::small_blob data;
+    bool choice;
+  };
+
+  template<typename F, typename T>
+  void basic_object_map(F& format, T& self)
+  {
+    wire::object(format, WIRE_FIELD(utf8), WIRE_FIELD(vec), WIRE_FIELD(data), WIRE_FIELD(choice));
+  }
+
+  template<typename T>
+  void read_bytes(wire::json_reader& source, basic_object<T>& dest)
+  { basic_object_map(source, dest); }
+
+  template<typename T>
+  void write_bytes(wire::json_writer& dest, const basic_object<T>& source)
+  { basic_object_map(dest, source); }
+
+  template<typename T>
+  void test_basic_reading(lest::env& lest_env)
+  {
+    SETUP("Basic values with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const auto result =
+        wire::json::from_bytes<basic_object<T>>(std::string{basic_json});
+      EXPECT(result);
+      EXPECT(result->utf8 == basic_string);
+      {
+        const std::vector<T> expected{0, 127};
+        EXPECT(result->vec == expected);
+      }
+      EXPECT(result->data == lws_test::blob_test1);
+      EXPECT(result->choice);
+    }
+  }
+
+  template<typename T>
+  void test_basic_writing(lest::env& lest_env)
+  {
+    SETUP("Basic values with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const basic_object<T> val{basic_string, std::vector<T>{0, 127}, lws_test::blob_test1, true};
+      const auto result = wire::json::to_bytes(val);
+      EXPECT(boost::range::equal(result, std::string{basic_json}));
+    }
+  }
+}
+
+LWS_CASE("wire::json_reader")
+{
+  using i64_limit = std::numeric_limits<std::int64_t>;
+  static constexpr const char negative_number[] = "-1";
+
+  test_basic_reading<std::int16_t>(lest_env);
+  test_basic_reading<std::int32_t>(lest_env);
+  test_basic_reading<std::int64_t>(lest_env);
+  test_basic_reading<std::intmax_t>(lest_env);
+  test_basic_reading<std::uint16_t>(lest_env);
+  test_basic_reading<std::uint32_t>(lest_env);
+  test_basic_reading<std::uint64_t>(lest_env);
+  test_basic_reading<std::uintmax_t>(lest_env);
+
+  static_assert(0 < i64_limit::max(), "expected 0 < int64_t::max");
+  static_assert(
+    i64_limit::max() <= std::numeric_limits<std::uintmax_t>::max(),
+    "expected int64_t::max <= uintmax_t::max"
+  );
+  std::string big_number = std::to_string(std::uintmax_t(i64_limit::max()) + 1);
+  EXPECT(wire::json::from_bytes<std::uint64_t>(negative_number) == wire::error::schema::larger_integer);
+  EXPECT(wire::json::from_bytes<std::int64_t>(std::move(big_number)) == wire::error::schema::smaller_integer);
+}
+
+LWS_CASE("wire::json_writer")
+{
+  test_basic_writing<std::int16_t>(lest_env);
+  test_basic_writing<std::int32_t>(lest_env);
+  test_basic_writing<std::int64_t>(lest_env);
+  test_basic_writing<std::intmax_t>(lest_env);
+  test_basic_writing<std::uint16_t>(lest_env);
+  test_basic_writing<std::uint32_t>(lest_env);
+  test_basic_writing<std::uint64_t>(lest_env);
+  test_basic_writing<std::uintmax_t>(lest_env);
+}
diff --git a/tests/unit/wire/read.test.cpp b/tests/unit/wire/read.test.cpp
new file mode 100644
index 0000000..c962669
--- /dev/null
+++ b/tests/unit/wire/read.test.cpp
@@ -0,0 +1,107 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "framework.test.h"
+
+#include <boost/core/demangle.hpp>
+#include <cstdint>
+#include <limits>
+#include "wire/read.h"
+
+namespace
+{
+  template<typename Target>
+  void test_unsigned_to_unsigned(lest::env& lest_env)
+  {
+    using limit = std::numeric_limits<Target>;
+    static constexpr const auto max =
+      std::numeric_limits<std::uintmax_t>::max();
+    static_assert(limit::is_integer, "expected integer");
+    static_assert(!limit::is_signed, "expected unsigned");
+
+    SETUP("uintmax_t to " + boost::core::demangle(typeid(Target).name()))
+    {
+      EXPECT(Target(0) == wire::integer::cast_unsigned<Target>(std::uintmax_t(0)));
+      EXPECT(limit::max() == wire::integer::cast_unsigned<Target>(std::uintmax_t(limit::max())));
+      if (limit::max() < max)
+      {
+        EXPECT_THROWS_AS(wire::integer::cast_unsigned<Target>(std::uintmax_t(limit::max()) + 1), wire::exception);
+        EXPECT_THROWS_AS(wire::integer::cast_unsigned<Target>(max), wire::exception);
+      }
+    }
+  }
+
+  template<typename Target>
+  void test_signed_to_signed(lest::env& lest_env)
+  {
+    using limit = std::numeric_limits<Target>;
+    static constexpr const auto min =
+      std::numeric_limits<std::intmax_t>::min();
+    static constexpr const auto max =
+      std::numeric_limits<std::intmax_t>::max();
+    static_assert(limit::is_integer, "expected integer");
+    static_assert(limit::is_signed, "expected signed");
+
+    SETUP("intmax_t to " + boost::core::demangle(typeid(Target).name()))
+    {
+      if (min < limit::min())
+      {
+        EXPECT_THROWS_AS(wire::integer::cast_signed<Target>(std::intmax_t(limit::min()) - 1), wire::exception);
+        EXPECT_THROWS_AS(wire::integer::cast_signed<Target>(min), wire::exception);
+      }
+      EXPECT(limit::min() == wire::integer::cast_signed<Target>(std::intmax_t(limit::min())));
+      EXPECT(Target(0) == wire::integer::cast_signed<Target>(std::intmax_t(0)));
+      EXPECT(limit::max() == wire::integer::cast_signed<Target>(std::intmax_t(limit::max())));
+      if (limit::max() < max)
+      {
+        EXPECT_THROWS_AS(wire::integer::cast_signed<Target>(std::intmax_t(limit::max()) + 1), wire::exception);
+        EXPECT_THROWS_AS(wire::integer::cast_signed<Target>(max), wire::exception);
+      }
+    }
+  }
+}
+
+
+LWS_CASE("wire::integer::cast_*")
+{
+  SETUP("unsigned to unsigned")
+  {
+    test_unsigned_to_unsigned<std::uint8_t>(lest_env);
+    test_unsigned_to_unsigned<std::uint16_t>(lest_env);
+    test_unsigned_to_unsigned<std::uint32_t>(lest_env);
+    test_unsigned_to_unsigned<std::uint64_t>(lest_env);
+    test_unsigned_to_unsigned<std::uintmax_t>(lest_env);
+  }
+  SETUP("signed to signed")
+  {
+    test_signed_to_signed<std::int8_t>(lest_env);
+    test_signed_to_signed<std::int16_t>(lest_env);
+    test_signed_to_signed<std::int32_t>(lest_env);
+    test_signed_to_signed<std::int64_t>(lest_env);
+    test_signed_to_signed<std::intmax_t>(lest_env);
+  }
+}
diff --git a/tests/unit/wire/read.write.test.cpp b/tests/unit/wire/read.write.test.cpp
new file mode 100644
index 0000000..598b0bd
--- /dev/null
+++ b/tests/unit/wire/read.write.test.cpp
@@ -0,0 +1,230 @@
+// Copyright (c) 2022, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "framework.test.h"
+
+#include <boost/core/demangle.hpp>
+#include <cstdint>
+#include <limits>
+#include <string>
+#include <vector>
+#include "wire.h"
+#include "wire/json.h"
+#include "wire/vector.h"
+
+#include "wire/base.test.h"
+
+namespace
+{
+  template<typename T>
+  using limit = std::numeric_limits<T>;
+
+  struct inner
+  {
+    std::uint32_t left;
+    std::uint32_t right;
+  };
+
+  template<typename F, typename T>
+  void inner_map(F& format, T& self)
+  {
+    wire::object(format, WIRE_FIELD(left), WIRE_FIELD(right));
+  }
+  WIRE_DEFINE_OBJECT(inner, inner_map)
+
+  struct complex
+  {
+    std::vector<inner> objects;
+    std::vector<std::int16_t> ints;
+    std::vector<std::uint64_t> uints;
+    std::vector<lws_test::small_blob> blobs;
+    std::vector<std::string> strings;
+    bool choice;
+  };
+
+  template<typename F, typename T>
+  void complex_map(F& format, T& self)
+  {
+    wire::object(format,
+      WIRE_FIELD(objects),
+      WIRE_FIELD(ints),
+      WIRE_FIELD(uints),
+      WIRE_FIELD(blobs),
+      WIRE_FIELD(strings),
+      WIRE_FIELD(choice)
+    );
+  }
+  WIRE_DEFINE_OBJECT(complex, complex_map)
+
+  void verify_initial(lest::env& lest_env, const complex& self)
+  {
+    EXPECT(self.objects.empty());
+    EXPECT(self.ints.empty());
+    EXPECT(self.uints.empty());
+    EXPECT(self.blobs.empty());
+    EXPECT(self.strings.empty());
+    EXPECT(self.choice == false);
+  }
+
+  void fill(complex& self)
+  {
+    self.objects = std::vector<inner>{inner{0, limit<std::uint32_t>::max()}, inner{100, 200}, inner{44444, 83434}};
+    self.ints = std::vector<std::int16_t>{limit<std::int16_t>::min(), limit<std::int16_t>::max(), 0, 31234};
+    self.uints = std::vector<std::uint64_t>{0, limit<std::uint64_t>::max(), 34234234, 33};
+    self.blobs = {lws_test::blob_test1, lws_test::blob_test2, lws_test::blob_test3};
+    self.strings = {"string1", "string2", "string3", "string4"};
+    self.choice = true;
+  }
+
+  void verify_filled(lest::env& lest_env, const complex& self)
+  {
+    EXPECT(self.objects.size() == 3);
+    EXPECT(self.objects.at(0).left == 0);
+    EXPECT(self.objects.at(0).right == limit<std::uint32_t>::max());
+    EXPECT(self.objects.at(1).left == 100);
+    EXPECT(self.objects.at(1).right == 200);
+    EXPECT(self.objects.at(2).left == 44444);
+    EXPECT(self.objects.at(2).right == 83434);
+
+    EXPECT(self.ints.size() == 4);
+    EXPECT(self.ints.at(0) == limit<std::int16_t>::min());
+    EXPECT(self.ints.at(1) == limit<std::int16_t>::max());
+    EXPECT(self.ints.at(2) == 0);
+    EXPECT(self.ints.at(3) == 31234);
+
+    EXPECT(self.uints.size() == 4);
+    EXPECT(self.uints.at(0) == 0);
+    EXPECT(self.uints.at(1) == limit<std::uint64_t>::max());
+    EXPECT(self.uints.at(2) == 34234234);
+    EXPECT(self.uints.at(3) == 33);
+
+    EXPECT(self.blobs.size() == 3);
+    EXPECT(self.blobs.at(0) == lws_test::blob_test1);
+    EXPECT(self.blobs.at(1) == lws_test::blob_test2);
+    EXPECT(self.blobs.at(2) == lws_test::blob_test3);
+
+    EXPECT(self.strings.size() == 4);
+    EXPECT(self.strings.at(0) == "string1");
+    EXPECT(self.strings.at(1) == "string2");
+    EXPECT(self.strings.at(2) == "string3");
+    EXPECT(self.strings.at(3) == "string4");
+
+    EXPECT(self.choice == true);
+  }
+
+  template<typename T>
+  void run_complex(lest::env& lest_env)
+  {
+    SETUP("Complex test for " + boost::core::demangle(typeid(T).name()))
+    {
+      complex base{};
+      verify_initial(lest_env, base);
+
+      {
+        const expect<epee::byte_slice> bytes = T::to_bytes(base);
+        EXPECT(bytes);
+
+        const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()});
+        EXPECT(derived);
+        verify_initial(lest_env, *derived);
+      }
+
+      fill(base);
+
+      {
+        const expect<epee::byte_slice> bytes = T::to_bytes(base);
+        EXPECT(bytes);
+
+        const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()});
+        EXPECT(derived);
+        verify_filled(lest_env, *derived);
+      }
+    }
+  }
+
+  struct big { std::int64_t value; };
+  struct small { std::int32_t value; };
+
+  template<typename F, typename T>
+  void big_map(F& format, T& self)
+  { wire::object(format, WIRE_FIELD(value)); }
+
+  template<typename F, typename T>
+  void small_map(F& format, T& self)
+  { wire::object(format, WIRE_FIELD(value)); }
+
+  WIRE_DEFINE_OBJECT(big, big_map)
+  WIRE_DEFINE_OBJECT(small, small_map)
+
+  template<typename T>
+  expect<small> round_trip(lest::env& lest_env, std::int64_t value)
+  {
+    expect<small> out = small{0};
+    SETUP("Testing round-trip with " + std::to_string(value))
+    {
+      const expect<epee::byte_slice> bytes = T::template to_bytes(big{value});
+      EXPECT(bytes);
+      out = T::template from_bytes<small>(std::string{bytes->begin(), bytes->end()});
+    }
+    return out;
+  }
+
+  template<typename T>
+  void not_overflow(lest::env& lest_env, std::int64_t value)
+  {
+    const expect<small> result = round_trip<T>(lest_env, value);
+    EXPECT(result);
+    EXPECT(result->value == value);
+  }
+
+  template<typename T>
+  void overflow(lest::env& lest_env, std::int64_t value, const std::error_code error)
+  {
+    const expect<small> result = round_trip<T>(lest_env, value);
+    EXPECT(result == error);
+  }
+
+  template<typename T>
+  void run_overflow(lest::env& lest_env)
+  {
+    SETUP("Overflow test for " + boost::core::demangle(typeid(T).name()))
+    {
+      not_overflow<T>(lest_env, limit<std::int32_t>::min());
+      not_overflow<T>(lest_env, 0);
+      not_overflow<T>(lest_env, limit<std::int32_t>::max());
+
+      overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::min()) - 1, wire::error::schema::larger_integer);
+      overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::max()) + 1, wire::error::schema::smaller_integer);
+    }
+  }
+}
+
+LWS_CASE("wire::reader and wire::writer")
+{
+  run_complex<wire::json>(lest_env);
+  run_overflow<wire::json>(lest_env);
+}