Forráskód Böngészése

Merge branch 'master' into refactor/expected

# Conflicts:
#	include/jvalidate/validation_visitor.h
Sam Jaffe 1 hete
szülő
commit
d1f4328e3f

+ 2 - 0
CMakeLists.txt

@@ -10,3 +10,5 @@ include_directories("include/")
 
 add_subdirectory("tests/")
 add_subdirectory("src/")
+
+add_custom_target(test COMMAND ctest --test-dir ${CMAKE_BINARY_DIR}/tests)

+ 2 - 2
README.md

@@ -125,7 +125,7 @@ For example:
   Draft04, and therefore is represented as:
   ```
   {"divisibleBy", {{schema::Version::Earliest, &Self::multipleOf},
-                   {schema::Version::Draft04, Removed}}},
+                     {schema::Version::Draft04, Removed}}},
   {"multipleOf",  {schema::Version::Draft04, &Self::multipleOf}}
   ```
 * A small number of rare constraints change their meaning when moving from
@@ -133,7 +133,7 @@ For example:
   sense to use different MakeConstraint functions for them.
   ```
   {"items", {{schema::Version::Earliest, &Self::itemsTupleOrVector},
-             {schema::Version::Draft2020_12, &Self::additionalItems}}}
+               {schema::Version::Draft2020_12, &Self::additionalItems}}}
   ```
 * Reserved keywords that have no meaning by themselves can use the Literal
   rule:

+ 3 - 0
include/jvalidate/adapters/jsoncpp.h

@@ -122,4 +122,7 @@ public:
   using JsonCppAdapter::SimpleAdapter::const_value;
   using JsonCppAdapter::SimpleAdapter::value;
 };
+
+template <typename JSON> JsonCppAdapter(JSON const &) -> JsonCppAdapter<JSON const>;
+template <typename JSON> JsonCppAdapter(JSON &) -> JsonCppAdapter<JSON>;
 }

+ 12 - 8
include/jvalidate/detail/simple_adapter.h

@@ -76,7 +76,7 @@ public:
   size_t size() const { return value_ ? value_->size() : 0; }
 
   Adapter operator[](size_t index) const {
-    if (index > size()) {
+    if (index >= size()) {
       return Adapter();
     }
     auto it = begin();
@@ -187,27 +187,31 @@ public:
     });
   }
 
-  auto operator<=>(SimpleAdapter const & rhs) const
+  friend bool operator==(SimpleAdapter const & lhs, SimpleAdapter const & rhs) {
+    return lhs.value_ == rhs.value_;
+  }
+
+  friend auto operator<=>(SimpleAdapter const & lhs, SimpleAdapter const & rhs)
     requires std::totally_ordered<JSON>
   {
     using ord = std::strong_ordering;
     // Optimization - first we compare pointers
-    if (value_ == rhs.value_) {
+    if (lhs.value_ == rhs.value_) {
       return ord::equal;
     }
     // TODO: Can I implement this as `return *value_ <=> *rhs.value_`?
-    if (value_ && rhs.value_) {
-      if (*value_ < *rhs.value_) {
+    if (lhs.value_ && rhs.value_) {
+      if (*lhs.value_ < *rhs.value_) {
         return ord::less;
       }
-      if (*rhs.value_ < *value_) {
+      if (*rhs.value_ < *lhs.value_) {
         return ord::greater;
       }
       return ord::equal;
     }
     // Treat JSON(null) and nullptr as equivalent
-    if (value_) {
-      return type() == Type::Null ? ord::equivalent : ord::greater;
+    if (lhs.value_) {
+      return lhs.type() == Type::Null ? ord::equivalent : ord::greater;
     }
     return rhs.type() == Type::Null ? ord::equivalent : ord::less;
   }

+ 11 - 9
include/jvalidate/regex.h

@@ -37,17 +37,14 @@ private:
 public:
   static std::string_view engine_name() { return "std::regex[ECMAScript]"; }
 
-  static bool is_regex(std::string_view regex) {
-    try {
-      [[maybe_unused]] std::regex _{std::string(regex)};
-      return true;
-    } catch (std::exception const &) { return false; }
-  }
+  static bool is_regex(std::string_view regex) try {
+    return (std::regex(std::string(regex)), true);
+  } catch (std::exception const &) { return false; }
 
-  bool search(std::string const & regex, std::string const & text) {
-    auto const & re = cache_.try_emplace(regex, regex).first->second;
+  bool search(std::string const & regex, std::string const & text) try {
+    std::regex const & re = cache_.try_emplace(regex, regex).first->second;
     return std::regex_search(text, re);
-  }
+  } catch (std::exception const &) { return false; }
 };
 }
 
@@ -105,10 +102,15 @@ public:
       }
     }
 
+    if (it->second == nullptr) {
+      return false; // Regex was invalid - and we cached that
+    }
+
     UErrorCode status = U_ZERO_ERROR;
     icu::UnicodeString const ucs = icu::UnicodeString::fromUTF8(icu::StringPiece(text));
     std::unique_ptr<icu::RegexMatcher> matcher(it->second->matcher(ucs, status));
     if (U_FAILURE(status)) {
+      // Realistically, never called
       return false;
     }
     return matcher->find(status);

+ 3 - 1
include/jvalidate/validation_visitor.h

@@ -57,10 +57,12 @@
   }
 
 namespace jvalidate {
+JVALIDATE_TRIBOOL_TYPE(StoreResults, ForValid, ForInvalid, ForAnything);
+
 template <Adapter Root, RegexEngine RE, typename ExtensionVisitor> class ValidationVisitor {
 private:
-  JVALIDATE_TRIBOOL_TYPE(StoreResults, ForValid, ForInvalid, ForAnything);
   using VisitedAnnotation = std::tuple<std::unordered_set<size_t>, std::unordered_set<std::string>>;
+  friend class ValidationVisitorTest;
 
 private:
   detail::Pointer where_;

+ 41 - 26
tests/CMakeLists.txt

@@ -20,30 +20,48 @@ FetchContent_MakeAvailable(json_schema_test_suite)
 
 include(GoogleTest)
 
-add_executable(annotation_test annotation_test.cxx)
-target_link_libraries(annotation_test GTest::gtest GTest::gmock jsoncpp_lib)
-gtest_discover_tests(annotation_test)
+set(JVALIDATE_UNIT_TESTS
+    annotation_test extension_test validation_visitor_test enum_test detail_test
+    regex_test adapter_test jsoncpp_adapter_test)
 
-add_executable(extension_test extension_test.cxx)
-target_link_libraries(extension_test GTest::gtest GTest::gmock jsoncpp_lib)
-gtest_discover_tests(extension_test)
+set(JVALIDATE_TESTS selfvalidate_test ${JVALIDATE_UNIT_TESTS})
 
-add_executable(selfvalidate_test selfvalidate_test.cxx)
-target_compile_definitions(selfvalidate_test PUBLIC JVALIDATE_USE_EXCEPTIONS)
-if (${json_schema_test_suite_POPULATED})
-target_compile_definitions(selfvalidate_test
-    PUBLIC
-    JVALIDATE_JSON_SCHEMA_TEST_SUITE_DIR="${json_schema_test_suite_SOURCE_DIR}"
-)
-endif()
-target_link_libraries(selfvalidate_test GTest::gtest GTest::gmock jsoncpp_lib CURL::libcurl)
+# Each test executable matches with its filename.
+foreach(CASE IN LISTS JVALIDATE_TESTS)
+  add_executable(${CASE} ${CASE}.cxx)
+endforeach()
+
+if (JVALIDATE_COVERAGE)
+  # Include all test files in a single executable for coverage purposes
+  add_executable(jvalidate_mono_test $<LIST:TRANSFORM,${JVALIDATE_TESTS},APPEND,.cxx>)
+  # Disable main() in other source files
+  target_compile_definitions(jvalidate_mono_test PRIVATE JVALIDATE_MONOTEST)
+  # Enable coverage
+  target_compile_options(jvalidate_mono_test PRIVATE -fprofile-instr-generate -fcoverage-mapping)
+  target_link_options(jvalidate_mono_test PRIVATE -fprofile-instr-generate -fcoverage-mapping)
 
-if (ICU_FOUND)
-  target_link_libraries(annotation_test ICU::uc ICU::i18n)
-  target_link_libraries(extension_test ICU::uc ICU::i18n)
-  target_link_libraries(selfvalidate_test ICU::uc ICU::i18n)
+  # Add to the list of tests in order to perform the rest of the test setup...
+  list(APPEND JVALIDATE_TESTS jvalidate_mono_test)
 endif()
 
+foreach(CASE IN LISTS JVALIDATE_TESTS)
+  target_link_libraries(${CASE} GTest::gtest GTest::gmock jsoncpp_lib CURL::libcurl)
+  target_compile_definitions(${CASE} PRIVATE JVALIDATE_USE_EXCEPTIONS)
+  if (${json_schema_test_suite_POPULATED})
+    target_compile_definitions(${CASE}
+        PUBLIC
+        JVALIDATE_JSON_SCHEMA_TEST_SUITE_DIR="${json_schema_test_suite_SOURCE_DIR}"
+    )
+  endif()
+
+  if (ICU_FOUND)
+    target_compile_definitions(${CASE} PUBLIC JVALIDATE_HAS_ICU=1)
+    target_link_libraries(${CASE} ICU::uc ICU::i18n)
+  else()
+    target_compile_definitions(${CASE} PUBLIC JVALIDATE_HAS_ICU=0)
+  endif()
+endforeach()
+
 string(
   JOIN ":"
   SelfValidateTest_Unsupported
@@ -55,19 +73,16 @@ string(
 set(SelfValidateTest_Unsupported_Suites "")
 set(SelfValidateTest_Unsupported_Cases "*leap second")
 
-if (ICU_FOUND)
-  target_compile_definitions(selfvalidate_test PUBLIC JVALIDATE_HAS_ICU=1)
-  target_link_libraries(annotation_test ICU::uc ICU::i18n)
-  target_link_libraries(extension_test ICU::uc ICU::i18n)
-  target_link_libraries(selfvalidate_test ICU::uc ICU::i18n)
-else()
-  target_compile_definitions(selfvalidate_test PUBLIC JVALIDATE_HAS_ICU=0)
+if (NOT ICU_FOUND)
   string(
     APPEND SelfValidateTest_Unsupported
     ":*optional_non_bmp_regex"
   )
 endif()
 
+foreach(CASE IN LISTS JVALIDATE_UNIT_TESTS)
+  gtest_discover_tests(${CASE})
+endforeach()
 gtest_discover_tests(selfvalidate_test
                      EXTRA_ARGS --json_suite_filter=-${SelfValidateTest_Unsupported_Suites}
                                 --json_case_filter=-${SelfValidateTest_Unsupported_Cases}

+ 239 - 0
tests/adapter_test.cxx

@@ -0,0 +1,239 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <json/value.h>
+
+#include <jvalidate/adapters/jsoncpp.h>
+#include <jvalidate/detail/array_iterator.h>
+#include <jvalidate/detail/object_iterator.h>
+
+using namespace jvalidate::adapter;
+
+using testing::Eq;
+using testing::Optional;
+using testing::StartsWith;
+
+TEST(AdapterTest, NullIsMaybeNull) {
+  Json::Value null = Json::nullValue;
+  EXPECT_TRUE(JsonCppAdapter(null).maybe_null(true));
+  EXPECT_TRUE(JsonCppAdapter(null).maybe_null(false));
+}
+
+TEST(AdapterTest, ZeroIsNotMaybeNull) {
+  Json::Value zero = 0;
+  EXPECT_FALSE(JsonCppAdapter(zero).maybe_null(true));
+  EXPECT_FALSE(JsonCppAdapter(zero).maybe_null(false));
+}
+
+TEST(AdapterTest, EmptyStringCanBeMaybeNull) {
+  Json::Value empty = "";
+  EXPECT_FALSE(JsonCppAdapter(empty).maybe_null(true));
+  EXPECT_TRUE(JsonCppAdapter(empty).maybe_null(false));
+}
+
+TEST(AdapterTest, BooleanIsMaybeBool) {
+  Json::Value json = true;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(true), Optional(true));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(false), Optional(true));
+}
+
+TEST(AdapterTest, IntegerIsNotMaybeBool) {
+  Json::Value json = 0;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(false), Eq(std::nullopt));
+}
+
+TEST(AdapterTest, StringLiteralCanBeMaybeBool) {
+  {
+    Json::Value json = "true";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(false), Optional(true));
+  }
+
+  {
+    Json::Value json = "false";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_boolean(false), Optional(false));
+  }
+}
+
+TEST(AdapterTest, IntegerIsMaybeInt) {
+  Json::Value json = 0;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Optional(0));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Optional(0));
+}
+
+TEST(AdapterTest, BooleanCanBeMaybeInt) {
+  {
+    Json::Value json = true;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Optional(1));
+  }
+  {
+    Json::Value json = false;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Optional(0));
+  }
+}
+
+TEST(AdapterTest, StringCanBeMaybeInt) {
+  {
+    Json::Value json = "1";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Optional(1));
+  }
+  {
+    Json::Value json = "1.5";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Eq(std::nullopt));
+  }
+}
+
+TEST(AdapterTest, NumberCanBeMaybeInt) {
+  {
+    Json::Value json = Json::realValue;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Optional(0));
+  }
+  {
+    Json::Value json = 1.5;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Eq(std::nullopt));
+  }
+  {
+    Json::Value json = 10000000000000000000000.0;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_integer(false), Eq(std::nullopt));
+  }
+}
+
+TEST(AdapterTest, NumberIsMaybeNumber) {
+  {
+    Json::Value json = Json::realValue;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Optional(0));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Optional(0));
+  }
+  {
+    Json::Value json = 1.5;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Optional(1.5));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Optional(1.5));
+  }
+}
+
+TEST(AdapterTest, IntIsMaybeNumber) {
+  Json::Value json = 1;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Optional(1));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Optional(1));
+}
+
+TEST(AdapterTest, BooleanIsNotMaybeNumber) {
+  Json::Value json = true;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Eq(std::nullopt));
+}
+
+TEST(AdapterTest, StringCanBeMaybeNumber) {
+  {
+    Json::Value json = "1";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Optional(1));
+  }
+  {
+    Json::Value json = "1.5";
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_number(false), Optional(1.5));
+  }
+}
+
+TEST(AdapterTest, StringIsMaybeString) {
+  Json::Value json = "lorem ipsum";
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Optional(Eq("lorem ipsum")));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(Eq("lorem ipsum")));
+}
+
+TEST(AdapterTest, NullCanBeMaybeString) {
+  Json::Value json = Json::nullValue;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(Eq("")));
+}
+
+TEST(AdapterTest, IntCanBeMaybeString) {
+  Json::Value json = 5;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(Eq("5")));
+}
+
+TEST(AdapterTest, NumberCanBeMaybeString) {
+  Json::Value json = 1.5;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(StartsWith("1.5")));
+}
+
+TEST(AdapterTest, BooleanCanBeMaybeString) {
+  {
+    Json::Value json = false;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(Eq("false")));
+  }
+  {
+    Json::Value json = true;
+    EXPECT_THAT(JsonCppAdapter(json).maybe_string(true), Eq(std::nullopt));
+    EXPECT_THAT(JsonCppAdapter(json).maybe_string(false), Optional(Eq("true")));
+  }
+}
+
+TEST(AdapterTest, ArrayIsMaybeArraySize) {
+  Json::Value json = Json::arrayValue;
+  json.append(1);
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(true), Optional(1));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(false), Optional(1));
+}
+
+TEST(AdapterTest, StringIsNotMaybeArraySize) {
+  Json::Value json = "hello";
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(false), Eq(std::nullopt));
+}
+
+TEST(AdapterTest, NullCanBeMaybeArraySize) {
+  Json::Value json = Json::nullValue;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(false), Optional(0));
+}
+
+TEST(AdapterTest, EmptyObjectCanBeMaybeArraySize) {
+  Json::Value json = Json::objectValue;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_array_size(false), Optional(0));
+}
+
+TEST(AdapterTest, ObjectIsMaybeObjectSize) {
+  Json::Value json = Json::objectValue;
+  json["A"] = 1;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(true), Optional(1));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(false), Optional(1));
+}
+
+TEST(AdapterTest, StringIsNotMaybeObjectSize) {
+  Json::Value json = "hello";
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(false), Eq(std::nullopt));
+}
+
+TEST(AdapterTest, NullCanBeMaybeObjectSize) {
+  Json::Value json = Json::nullValue;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(false), Optional(0));
+}
+
+TEST(AdapterTest, EmptyArrayCanBeMaybeObjectSize) {
+  Json::Value json = Json::arrayValue;
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(true), Eq(std::nullopt));
+  EXPECT_THAT(JsonCppAdapter(json).maybe_object_size(false), Optional(0));
+}
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif

+ 107 - 14
tests/annotation_test.cxx

@@ -9,14 +9,15 @@
 #include <jvalidate/uri.h>
 #include <jvalidate/validation_result.h>
 #include <jvalidate/validator.h>
+#include <sstream>
 
 #include "matchers.h"
 
 using enum jvalidate::schema::Version;
 using testing::Not;
 
-auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
-              jvalidate::schema::Version version = Draft2020_12) {
+static auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
+                     jvalidate::schema::Version version = Draft2020_12) {
   jvalidate::Schema const schema(schema_doc, version);
 
   jvalidate::ValidationResult result;
@@ -25,7 +26,7 @@ auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
   return result;
 }
 
-TEST(Annotation, AttachesFormattingAnnotation) {
+TEST(AnnotationTest, AttachesFormattingAnnotation) {
   auto const schema = R"({
     "format": "uri"
   })"_json;
@@ -37,7 +38,17 @@ TEST(Annotation, AttachesFormattingAnnotation) {
   EXPECT_THAT(result, AnnotationAt("format", "uri"));
 }
 
-TEST(Annotation, AnnotatesErrors) {
+TEST(AnnotationTest, AnnotatesAtRootSafely) {
+  auto const schema = R"(false)"_json;
+
+  auto const instance = R"(4)"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+
+  EXPECT_THAT(result, ErrorAt("", "always false"));
+}
+
+TEST(AnnotationTest, AnnotatesErrors) {
   auto const schema = R"({
     "minimum": 5
   })"_json;
@@ -49,7 +60,42 @@ TEST(Annotation, AnnotatesErrors) {
   EXPECT_THAT(result, ErrorAt("minimum", "4 < 5"));
 }
 
-TEST(Annotation, DoesNotAnnotatesValid) {
+TEST(AnnotationTest, AnnotatesDescription) {
+  auto const schema = R"({
+    "description": "lorem ipsum",
+    "minimum": 5
+  })"_json;
+
+  auto const instance = R"(4)"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+
+  EXPECT_THAT(result, AnnotationAt("description", "lorem ipsum"));
+}
+
+TEST(AnnotationTest, CanRecordAnnotationsAsList) {
+  auto const schema = R"({
+    "items": false
+  })"_json;
+
+  auto const instance = R"([ 0, 1 ])"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+  EXPECT_THAT(result, AnnotationAt("items", std::vector<std::string>{"0", "1"}));
+}
+
+TEST(AnnotationTest, OnlyErrors) {
+  auto const schema = R"({
+    "items": false
+  })"_json;
+
+  auto const instance = R"([ 0, 1 ])"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance).only_errors();
+  EXPECT_THAT(result, Not(HasAnnotationAt(""_jptr, ""_jptr)));
+}
+
+TEST(AnnotationTest, DoesNotAnnotatesValid) {
   auto const schema = R"({
     "minimum": 5
   })"_json;
@@ -61,7 +107,7 @@ TEST(Annotation, DoesNotAnnotatesValid) {
   EXPECT_THAT(result, Not(HasAnnotationsFor(""_jptr)));
 }
 
-TEST(Annotation, NotSchemaFlipsAnnotationRule) {
+TEST(AnnotationTest, NotSchemaFlipsAnnotationRule) {
   auto const schema = R"({
     "not": { "minimum": 5 }
   })"_json;
@@ -73,7 +119,7 @@ TEST(Annotation, NotSchemaFlipsAnnotationRule) {
   EXPECT_THAT(result, ErrorAt(""_jptr, "/not"_jptr, "minimum", "6 >= 5"));
 }
 
-TEST(Annotation, NotSchemaPropogatesDeeply) {
+TEST(AnnotationTest, NotSchemaPropogatesDeeply) {
   auto const schema = R"({
     "not": {
       "properties": {
@@ -91,7 +137,7 @@ TEST(Annotation, NotSchemaPropogatesDeeply) {
   EXPECT_THAT(result, ErrorAt("/A"_jptr, "/not/properties/A"_jptr, "enum", "1"));
 }
 
-TEST(Annotation, PathFollowsSchemaNotConstraintModel) {
+TEST(AnnotationTest, PathFollowsSchemaNotConstraintModel) {
   auto const schema = R"({
     "$comment": "disallow is implemented in the form of NotConstraint[TypeConstraint]",
     "disallow": "string"
@@ -104,7 +150,7 @@ TEST(Annotation, PathFollowsSchemaNotConstraintModel) {
   EXPECT_THAT(result, ErrorAt("disallow", "string is in types [string]"));
 }
 
-TEST(Annotation, AttachesAlwaysFalseSensibly) {
+TEST(AnnotationTest, AttachesAlwaysFalseSensibly) {
   auto const schema = R"({
     "properties": {
       "A": false
@@ -119,7 +165,7 @@ TEST(Annotation, AttachesAlwaysFalseSensibly) {
   EXPECT_THAT(result, ErrorAt("/A"_jptr, "/properties/A"_jptr, "", "always false"));
 }
 
-TEST(Annotation, IfThenCanGiveABecauseReason) {
+TEST(AnnotationTest, IfThenCanGiveABecauseReason) {
   auto const schema = R"({
     "if": {
       "properties": {
@@ -153,7 +199,7 @@ TEST(Annotation, IfThenCanGiveABecauseReason) {
                               "4 is not a multiple of 3"));
 }
 
-TEST(Annotation, IfElseCanGiveABecauseReason) {
+TEST(AnnotationTest, IfElseCanGiveABecauseReason) {
   auto const schema = R"({
     "if": {
       "properties": {
@@ -187,7 +233,7 @@ TEST(Annotation, IfElseCanGiveABecauseReason) {
                               "4 is not a multiple of 5"));
 }
 
-TEST(Annotation, OneOfGivesAllReasonsOnTooMany) {
+TEST(AnnotationTest, OneOfGivesAllReasonsOnTooMany) {
   auto const schema = R"({
     "oneOf": [
       { "minimum": 5 },
@@ -206,7 +252,7 @@ TEST(Annotation, OneOfGivesAllReasonsOnTooMany) {
   EXPECT_THAT(result, ErrorAt(""_jptr, "/oneOf/2"_jptr, "maximum", "6 > 2"));
 }
 
-TEST(Annotation, OneOfGivesAllReasonsOnNoMatches) {
+TEST(AnnotationTest, OneOfGivesAllReasonsOnNoMatches) {
   auto const schema = R"({
     "oneOf": [
       { "minimum": 5 },
@@ -225,7 +271,7 @@ TEST(Annotation, OneOfGivesAllReasonsOnNoMatches) {
   EXPECT_THAT(result, ErrorAt(""_jptr, "/oneOf/2"_jptr, "maximum", "4 > 2"));
 }
 
-TEST(Annotation, OneOfGivesAllReasonsOnMatch) {
+TEST(AnnotationTest, OneOfGivesAllReasonsOnMatch) {
   auto const schema = R"({
     "oneOf": [
       { "minimum": 5 },
@@ -242,7 +288,54 @@ TEST(Annotation, OneOfGivesAllReasonsOnMatch) {
   EXPECT_THAT(result, ErrorAt(""_jptr, "/oneOf/1"_jptr, "multipleOf", "3 is a multiple of 3"));
 }
 
+TEST(ValidationResultJsonTest, OutputsFormattedJSON) {
+  auto const schema = R"({
+    "items": false
+  })"_json;
+
+  auto const instance = R"([ 0, 1 ])"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+  std::stringstream ss;
+  ss << result;
+
+  EXPECT_THAT(ss.str(), R"({
+  "valid": false,
+  "details": [
+    {
+      "valid": false,
+      "evaluationPath": "",
+      "instanceLocation": "",
+      "annotations": {
+        "items": [
+          "0",
+          "1"
+        ]
+      }
+    },
+    {
+      "valid": false,
+      "evaluationPath": "/items",
+      "instanceLocation": "/0",
+      "errors": {
+        "": "always false"
+      }
+    },
+    {
+      "valid": false,
+      "evaluationPath": "/items",
+      "instanceLocation": "/1",
+      "errors": {
+        "": "always false"
+      }
+    }
+  ]
+})");
+}
+
+#if !defined(JVALIDATE_MONOTEST)
 int main(int argc, char ** argv) {
   testing::InitGoogleMock(&argc, argv);
   return RUN_ALL_TESTS();
 }
+#endif

+ 242 - 0
tests/detail_test.cxx

@@ -0,0 +1,242 @@
+#include <stdexcept>
+#include <type_traits>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <jvalidate/adapters/jsoncpp.h>
+#include <jvalidate/detail/anchor.h>
+#include <jvalidate/detail/number.h>
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/detail/reference.h>
+#include <jvalidate/detail/relative_pointer.h>
+#include <jvalidate/detail/string.h>
+#include <jvalidate/detail/string_adapter.h>
+#include <jvalidate/enum.h>
+
+#include "matchers.h"
+
+using namespace jvalidate::detail;
+using JsonCppAdapter = jvalidate::adapter::JsonCppAdapter<Json::Value const>;
+using jvalidate::adapter::Type;
+
+using testing::Eq;
+using testing::Test;
+using testing::ThrowsMessage;
+using testing::Types;
+using testing::VariantWith;
+
+TEST(AnchorTest, PermitsEmpty) { EXPECT_NO_THROW(Anchor("")); }
+
+TEST(AnchorTest, PermitsFirstCharWord) { EXPECT_NO_THROW(Anchor("here")); }
+
+TEST(AnchorTest, PermitsFirstCharUnderscore) { EXPECT_NO_THROW(Anchor("_here")); }
+
+TEST(AnchorTest, ForbidsFirstCharNumber) { EXPECT_THROW(Anchor("0here"), std::runtime_error); }
+
+TEST(AnchorTest, AllowsDotDashAlnum) { EXPECT_NO_THROW(Anchor("here.0-_")); }
+
+TEST(AnchorTest, ForbidsSpecial) { EXPECT_THROW(Anchor("h$"), std::runtime_error); }
+
+TEST(AnchorTest, CanViewString) { EXPECT_THAT(static_cast<std::string>(Anchor("here")), "here"); }
+
+TEST(AnchorTest, Print) { EXPECT_THAT(testing::PrintToString(Anchor("here")), "here"); }
+
+TEST(PointerTest, BackCoercesIntToString) {
+  EXPECT_THAT("/0"_jptr.back(), "0");
+  EXPECT_THAT("/A"_jptr.back(), "A");
+}
+
+TEST(PointerTest, BackIsEmptySafe) { EXPECT_THAT(""_jptr.back(), ""); }
+
+TEST(PointerTest, ForbidsBadTilde) {
+  EXPECT_NO_THROW("/~1"_jptr);
+  EXPECT_THROW("/~2"_jptr, std::runtime_error);
+}
+
+TEST(PointerTest, CanConcatenate) { EXPECT_THAT("/A"_jptr / "/B"_jptr, "/A/B"_jptr); }
+
+TEST(PointerTest, CanGoToParent) { EXPECT_THAT("/A/B"_jptr / parent, "/A"_jptr); }
+
+TEST(PointerTest, Print) { EXPECT_THAT(testing::PrintToString(Pointer("/B/0/A")), "/B/0/A"); }
+
+TEST(RelatvivePointerTest, CannotPrefixWithZero) {
+  EXPECT_THROW(RelativePointer("01"), std::runtime_error);
+}
+
+TEST(RelatvivePointerTest, Print) {
+  EXPECT_THAT(testing::PrintToString(RelativePointer("0")), "0");
+  EXPECT_THAT(testing::PrintToString(RelativePointer("1#")), "1#");
+  EXPECT_THAT(testing::PrintToString(RelativePointer("1/B/0/A")), "1/B/0/A");
+}
+
+TEST(RelatvivePointerTest, ZeroIsHere) {
+  Json::Value json;
+  json["A"] = 1;
+  RelativePointer const rel("0");
+  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["A"])))
+      << rel;
+}
+
+TEST(RelatvivePointerTest, CanWalkBackwards) {
+  Json::Value json;
+  json["A"] = 1;
+  RelativePointer const rel("1");
+  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json)))
+      << rel;
+}
+
+TEST(RelatvivePointerTest, CanFetchKey) {
+  Json::Value json;
+  json["A"] = 1;
+  RelativePointer const rel("0#");
+  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)), VariantWith<std::string>(Eq("A")))
+      << rel;
+}
+
+TEST(RelatvivePointerTest, CanGoUpAndDown) {
+  Json::Value json;
+  json["A"] = 1;
+  json["B"] = 2;
+  RelativePointer const rel("1/B");
+  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["B"])))
+      << rel;
+}
+
+TEST(ReferenceTest, Print) {
+  EXPECT_THAT(testing::PrintToString(Reference(jvalidate::URI("file://path/to/document.json"),
+                                               Anchor("Anchor"), Pointer("/key/1/id"))),
+              "file://path/to/document.json#Anchor/key/1/id");
+}
+
+TEST(ReferenceTest, RelativeBasedOnURIRelative) {
+  {
+    jvalidate::URI uri("file://path/to/document.json");
+    EXPECT_FALSE(uri.is_relative());
+    EXPECT_THAT(RootReference(uri).is_relative(), uri.is_relative());
+  }
+  {
+    jvalidate::URI uri("/path/to/document.json");
+    EXPECT_FALSE(uri.is_relative());
+    EXPECT_THAT(RootReference(uri).is_relative(), uri.is_relative());
+  }
+  {
+    jvalidate::URI uri("path/to/document.json");
+    EXPECT_TRUE(uri.is_relative());
+    EXPECT_THAT(RootReference(uri).is_relative(), uri.is_relative());
+  }
+}
+
+template <typename T> class NumberFromStrTest : public Test {};
+
+using Integers = Types<char, size_t, int>;
+TYPED_TEST_SUITE(NumberFromStrTest, Integers);
+
+TEST(NumberTest, ULLFitsInInteger) {
+  EXPECT_TRUE(fits_in_integer(9223372036854775807ULL));
+  EXPECT_FALSE(fits_in_integer(9223372036854775808ULL));
+}
+
+TEST(NumberTest, DoubleDoesNotFitInIntegerWhenFractional) { EXPECT_FALSE(fits_in_integer(10.5)); }
+
+TEST(NumberTest, DoubleFitsInIntegerWhenWhole) { EXPECT_TRUE(fits_in_integer(10.0)); }
+
+TEST(NumberTest, DoubleOutOfIntegerRange) {
+  EXPECT_FALSE(fits_in_integer(10000000000000000000.0));
+  EXPECT_FALSE(fits_in_integer(-10000000000000000000.0));
+}
+
+TYPED_TEST(NumberFromStrTest, NumberParsesIntegers) { EXPECT_THAT(from_str<TypeParam>("10"), 10); }
+
+TYPED_TEST(NumberFromStrTest, NumberParsesNegativeIntegers) {
+  if constexpr (std::is_signed_v<TypeParam>) {
+    EXPECT_THAT(from_str<TypeParam>("-10"), -10);
+  } else {
+    EXPECT_THAT([] { from_str<TypeParam>("-10"); },
+                ThrowsMessage<std::runtime_error>("Invalid argument"));
+  }
+}
+
+TYPED_TEST(NumberFromStrTest, ThrowsOnPlusSign) {
+  EXPECT_THAT([] { from_str<TypeParam>("+10"); },
+              ThrowsMessage<std::runtime_error>("Invalid argument"));
+}
+
+TYPED_TEST(NumberFromStrTest, ThrowsOnTooManyChars) {
+  EXPECT_THAT([] { from_str<TypeParam>("10 coconuts"); },
+              ThrowsMessage<std::runtime_error>("NaN: 10 coconuts"));
+}
+
+TYPED_TEST(NumberFromStrTest, ThrowsOnOutOfRange) {
+  EXPECT_THAT([] { from_str<TypeParam>("99999999999999999999999999999"); },
+              ThrowsMessage<std::runtime_error>("Result too large"));
+}
+
+TEST(StringAdapterTest, IsStringy) {
+  EXPECT_THAT(StringAdapter("").type(), Type::String);
+  EXPECT_THAT(StringAdapter("lorem ipsum").as_string(), "lorem ipsum");
+}
+
+TEST(StringAdapterTest, DiesOnAccess) {
+  EXPECT_THROW(StringAdapter("").as_boolean(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").as_integer(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").as_number(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").as_array(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").array_size(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").as_object(), std::runtime_error);
+  EXPECT_THROW(StringAdapter("").object_size(), std::runtime_error);
+}
+
+TEST(StringAdapterTest, DoesNotRunApplyArray) {
+  StringAdapter("").apply_array([](auto const &) {
+    ADD_FAILURE();
+    return jvalidate::Status::Noop;
+  });
+}
+
+TEST(StringAdapterTest, DoesNotRunApplyObject) {
+  StringAdapter("").apply_object([](auto const &, auto const &) {
+    ADD_FAILURE();
+    return jvalidate::Status::Noop;
+  });
+}
+
+TEST(UnsupportedArrayAdapterTest, Empty) {
+  UnsupportedArrayAdapter<StringAdapter> array;
+  EXPECT_THAT(array.size(), 0);
+  EXPECT_THAT(array.begin(), array.end());
+}
+
+TEST(UnsupportedArrayAdapterTest, DiesWhenIndexed) {
+  UnsupportedArrayAdapter<StringAdapter> array;
+  EXPECT_THROW(array[0], std::runtime_error);
+}
+
+TEST(UnsupportedObjectAdapterTest, Empty) {
+  UnsupportedObjectAdapter<StringAdapter> object;
+  EXPECT_THAT(object.size(), 0);
+  EXPECT_THAT(object.begin(), object.end());
+}
+
+TEST(UnsupportedObjectAdapterTest, DiesWhenIndexed) {
+  UnsupportedObjectAdapter<StringAdapter> object;
+  EXPECT_FALSE(object.contains(""));
+  EXPECT_THROW(object[""], std::runtime_error);
+}
+
+#if JVALIDATE_HAS_ICU
+TEST(StringLengthTest, MultiCharLength) {
+  // Dragon Emoji from non-bmp-regex test case
+  EXPECT_THAT(length("\xF0\x9F\x90\xB2"), 1);
+}
+#endif
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif

+ 101 - 0
tests/enum_test.cxx

@@ -0,0 +1,101 @@
+#include <compare>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <jvalidate/status.h>
+#include <jvalidate/validation_visitor.h>
+
+using testing::Eq;
+using testing::Not;
+
+TEST(StatusTest, Spaceship) {
+  using jvalidate::Status;
+  using enum jvalidate::Status::Enum;
+  EXPECT_THAT(Status(Accept) <=> Status(Reject), Not(std::strong_ordering::equal));
+}
+
+TEST(StatusTest, Not) {
+  using jvalidate::Status;
+  using enum jvalidate::Status::Enum;
+  EXPECT_THAT(!Status(Accept), Eq(Reject));
+  EXPECT_THAT(!Status(Reject), Eq(Accept));
+  EXPECT_THAT(!Status(Noop), Eq(Noop));
+}
+
+TEST(StatusTest, Or) {
+  using jvalidate::Status;
+  using enum jvalidate::Status::Enum;
+  EXPECT_THAT(Status(Accept) | Status(Accept), Eq(Accept));
+  EXPECT_THAT(Status(Accept) | Status(Noop), Eq(Accept));
+  EXPECT_THAT(Status(Accept) | Status(Reject), Eq(Accept));
+  EXPECT_THAT(Status(Noop) | Status(Accept), Eq(Accept));
+  EXPECT_THAT(Status(Noop) | Status(Noop), Eq(Noop));
+  EXPECT_THAT(Status(Noop) | Status(Reject), Eq(Noop));
+  EXPECT_THAT(Status(Reject) | Status(Accept), Eq(Accept));
+  EXPECT_THAT(Status(Reject) | Status(Noop), Eq(Noop));
+  EXPECT_THAT(Status(Reject) | Status(Reject), Eq(Reject));
+}
+
+TEST(StatusTest, And) {
+  using jvalidate::Status;
+  using enum jvalidate::Status::Enum;
+  EXPECT_THAT(Status(Accept) & Status(Accept), Eq(Accept));
+  EXPECT_THAT(Status(Accept) & Status(Noop), Eq(Accept));
+  EXPECT_THAT(Status(Accept) & Status(Reject), Eq(Reject));
+  EXPECT_THAT(Status(Noop) & Status(Accept), Eq(Accept));
+  EXPECT_THAT(Status(Noop) & Status(Noop), Eq(Noop));
+  EXPECT_THAT(Status(Noop) & Status(Reject), Eq(Reject));
+  EXPECT_THAT(Status(Reject) & Status(Accept), Eq(Reject));
+  EXPECT_THAT(Status(Reject) & Status(Noop), Eq(Reject));
+  EXPECT_THAT(Status(Reject) & Status(Reject), Eq(Reject));
+}
+
+TEST(StoreResultsTest, Spaceship) {
+  using jvalidate::StoreResults;
+  using enum jvalidate::StoreResults::Enum;
+  EXPECT_THAT(StoreResults(ForValid) <=> StoreResults(ForInvalid),
+              Not(std::strong_ordering::equal));
+}
+
+TEST(StoreResultsTest, Not) {
+  using jvalidate::StoreResults;
+  using enum jvalidate::StoreResults::Enum;
+  EXPECT_THAT(!StoreResults(ForValid), Eq(ForInvalid));
+  EXPECT_THAT(!StoreResults(ForInvalid), Eq(ForValid));
+  EXPECT_THAT(!StoreResults(ForAnything), Eq(ForAnything));
+}
+
+TEST(StoreResultsTest, Or) {
+  using jvalidate::StoreResults;
+  using enum jvalidate::StoreResults::Enum;
+  EXPECT_THAT(StoreResults(ForValid) | StoreResults(ForValid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForValid) | StoreResults(ForAnything), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForValid) | StoreResults(ForInvalid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForAnything) | StoreResults(ForValid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForAnything) | StoreResults(ForAnything), Eq(ForAnything));
+  EXPECT_THAT(StoreResults(ForAnything) | StoreResults(ForInvalid), Eq(ForAnything));
+  EXPECT_THAT(StoreResults(ForInvalid) | StoreResults(ForValid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForInvalid) | StoreResults(ForAnything), Eq(ForAnything));
+  EXPECT_THAT(StoreResults(ForInvalid) | StoreResults(ForInvalid), Eq(ForInvalid));
+}
+
+TEST(StoreResultsTest, And) {
+  using jvalidate::StoreResults;
+  using enum jvalidate::StoreResults::Enum;
+  EXPECT_THAT(StoreResults(ForValid) & StoreResults(ForValid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForValid) & StoreResults(ForAnything), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForValid) & StoreResults(ForInvalid), Eq(ForInvalid));
+  EXPECT_THAT(StoreResults(ForAnything) & StoreResults(ForValid), Eq(ForValid));
+  EXPECT_THAT(StoreResults(ForAnything) & StoreResults(ForAnything), Eq(ForAnything));
+  EXPECT_THAT(StoreResults(ForAnything) & StoreResults(ForInvalid), Eq(ForInvalid));
+  EXPECT_THAT(StoreResults(ForInvalid) & StoreResults(ForValid), Eq(ForInvalid));
+  EXPECT_THAT(StoreResults(ForInvalid) & StoreResults(ForAnything), Eq(ForInvalid));
+  EXPECT_THAT(StoreResults(ForInvalid) & StoreResults(ForInvalid), Eq(ForInvalid));
+}
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif

+ 6 - 4
tests/extension_test.cxx

@@ -43,8 +43,8 @@ public:
   }
 };
 
-auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
-              jvalidate::schema::Version version = Draft2020_12) {
+static auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
+                     jvalidate::schema::Version version = Draft2020_12) {
   using A = jvalidate::adapter::AdapterFor<Json::Value const>;
   jvalidate::ConstraintFactory<A> factory{{"is_key_of", [](auto const & context) {
                                              return ExtensionConstraint::make<IsKeyOfConstraint>(
@@ -58,7 +58,7 @@ auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
   return result;
 }
 
-TEST(ExtensionConstraint, CanReportSuccess) {
+TEST(ExtensionConstraintTest, CanReportSuccess) {
   auto schema = R"({
     "properties": {
       "nodes": {
@@ -98,7 +98,7 @@ TEST(ExtensionConstraint, CanReportSuccess) {
   EXPECT_THAT(result, Valid());
 }
 
-TEST(ExtensionConstraint, CanReportFailure) {
+TEST(ExtensionConstraintTest, CanReportFailure) {
   auto schema = R"({
     "properties": {
       "nodes": {
@@ -138,7 +138,9 @@ TEST(ExtensionConstraint, CanReportFailure) {
   EXPECT_THAT(result, Not(Valid()));
 }
 
+#if !defined(JVALIDATE_MONOTEST)
 int main(int argc, char ** argv) {
   testing::InitGoogleMock(&argc, argv);
   return RUN_ALL_TESTS();
 }
+#endif

+ 75 - 0
tests/jsoncpp_adapter_test.cxx

@@ -0,0 +1,75 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <json/value.h>
+
+#include <jvalidate/adapter.h>
+#include <jvalidate/adapters/jsoncpp.h>
+#include <jvalidate/detail/array_iterator.h>
+#include <jvalidate/detail/object_iterator.h>
+
+using namespace jvalidate::adapter;
+using testing::IsEmpty;
+
+TEST(JsonCppAdapterTest, IteratorDefaultCtors) {
+  using ArrayIterator =
+      detail::JsonArrayIterator<Json::ValueConstIterator, JsonCppAdapter<Json::Value const>>;
+  using ObjectIterator =
+      detail::JsonObjectIterator<Json::ValueConstIterator, JsonCppAdapter<Json::Value const>>;
+
+  EXPECT_THAT(ArrayIterator(), ArrayIterator());
+  EXPECT_THAT(ObjectIterator(), ObjectIterator());
+}
+
+TEST(JsonCppAdapterTest, Empty) {
+  EXPECT_THAT(&AdapterTraits<Json::Value>::const_empty(),
+              &AdapterTraits<Json::Value const>::const_empty());
+}
+
+TEST(JsonCppAdapterTest, NumberThatFitsInIntIsIntType) {
+  Json::Value json = 10.0;
+  EXPECT_THAT(JsonCppAdapter(json).type(), Type::Integer);
+}
+
+TEST(JsonCppAdapterTest, NumberThatDoesFitsInIntIsNumberType) {
+  Json::Value json = 10.5;
+  EXPECT_THAT(JsonCppAdapter(json).type(), Type::Number);
+}
+
+TEST(JsonCppAdapterTest, UIntThatFitsInIntIsIntType) {
+  Json::Value json = 10ULL;
+  EXPECT_THAT(JsonCppAdapter(json).type(), Type::Integer);
+}
+
+TEST(JsonCppAdapterTest, UIntThatDoesNotFitInIntIsNumberType) {
+  Json::Value json = 9223372036854775808ULL;
+  EXPECT_THAT(JsonCppAdapter(json).type(), Type::Number);
+}
+
+TEST(JsonCppAdapterTest, ObjectAdapterIsNullSafe) {
+  JsonCppObjectAdapter<Json::Value const> object(nullptr);
+  EXPECT_THAT(object.size(), 0);
+  EXPECT_THAT(*object, IsEmpty());
+  EXPECT_NO_THROW(object.find("A"));
+  EXPECT_THAT(object.begin(), object.end());
+  EXPECT_THAT(object.find("A"), object.end());
+  EXPECT_FALSE(object.contains("A"));
+  EXPECT_NO_THROW(object["A"]);
+  EXPECT_THAT(object["A"].type(), Type::Null);
+}
+
+TEST(JsonCppAdapterTest, ArrayAdapterIsNullSafe) {
+  jvalidate::adapter::detail::SimpleArrayAdapter<Json::Value const> array(nullptr);
+  EXPECT_THAT(array.size(), 0);
+  EXPECT_THAT(*array, IsEmpty());
+  EXPECT_THAT(array.begin(), array.end());
+  EXPECT_NO_THROW(array[0]);
+  EXPECT_THAT(array[0].type(), Type::Null);
+}
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif

+ 1 - 2
tests/matchers.h

@@ -2,7 +2,6 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
-#include <ios>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/validation_result.h>
 
@@ -13,7 +12,7 @@ inline auto operator""_jptr(char const * data, size_t len) {
   return jvalidate::detail::Pointer::parse(std::string_view{data, len}).value();
 }
 
-inline Json::Value operator""_json(char const * data, size_t len) {
+inline Json::Value const operator""_json(char const * data, size_t len) {
   Json::Value value;
 
   Json::CharReaderBuilder builder;

+ 58 - 0
tests/regex_test.cxx

@@ -0,0 +1,58 @@
+#include "jvalidate/regex.h"
+
+#include "gtest/gtest.h"
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using testing::IsEmpty;
+using testing::Not;
+
+template <typename T> class RegexEngineTest : public testing::Test {};
+
+using RegexEngines = testing::Types<jvalidate::StdRegexEngine
+#if JVALIDATE_HAS_ICU
+                                    ,
+                                    jvalidate::ICURegexEngine
+#endif
+                                    >;
+
+TYPED_TEST_SUITE(RegexEngineTest, RegexEngines);
+
+TYPED_TEST(RegexEngineTest, HasEngineName) {
+  EXPECT_THAT(TypeParam::engine_name(), Not(IsEmpty()));
+}
+
+TYPED_TEST(RegexEngineTest, IsRegexIsNoexceptOnBadRegex) {
+  EXPECT_NO_THROW(TypeParam::is_regex("(ABC){1,2"));
+  EXPECT_FALSE(TypeParam::is_regex("(ABC){1,2"));
+}
+
+TYPED_TEST(RegexEngineTest, IsRegexIsNoexceptOnGoodRegex) {
+  EXPECT_NO_THROW(TypeParam::is_regex("(ABC){1,2}"));
+  EXPECT_TRUE(TypeParam::is_regex("(ABC){1,2}"));
+}
+
+TYPED_TEST(RegexEngineTest, SearchCanMatchSubstring) {
+  EXPECT_NO_THROW(TypeParam().search("\\d", "10 dollars"));
+  EXPECT_TRUE(TypeParam().search("\\d", "10 dollars"));
+}
+
+TYPED_TEST(RegexEngineTest, SearchCanSetBoundaries) {
+  EXPECT_NO_THROW(TypeParam().search("^\\d$", "10 dollars"));
+  EXPECT_FALSE(TypeParam().search("^\\d$", "10 dollars"));
+}
+
+TYPED_TEST(RegexEngineTest, SearchIsNoexceptOnBadRegex) {
+  TypeParam engine;
+  EXPECT_NO_THROW(engine.search("(ABC){1,2", "ABC"));
+  EXPECT_FALSE(engine.search("(ABC){1,2", "ABC"));
+  // Repeated calls *can* use a cached result, even if compilation failed
+  EXPECT_FALSE(engine.search("(ABC){1,2", "ABC"));
+}
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif

+ 10 - 10
tests/selfvalidate_test.cxx

@@ -38,7 +38,7 @@ bool load_external_for_test(jvalidate::URI const & uri, Json::Value & out,
   return jvalidate::curl_get(uri, out, error);
 }
 
-class JsonSchema : public TestWithParam<SchemaParams> {
+class JsonSchemaTest : public TestWithParam<SchemaParams> {
 private:
   static RecursiveTestFilter s_suite_filter;
   static RecursiveTestFilter s_case_filter;
@@ -91,10 +91,10 @@ protected:
   }
 };
 
-RecursiveTestFilter JsonSchema::s_suite_filter;
-RecursiveTestFilter JsonSchema::s_case_filter;
+RecursiveTestFilter JsonSchemaTest::s_suite_filter;
+RecursiveTestFilter JsonSchemaTest::s_case_filter;
 
-TEST_P(JsonSchema, TestSuite) {
+TEST_P(JsonSchemaTest, TestSuite) {
   auto const & [version, file] = GetParam();
   Json::Value spec;
 
@@ -125,13 +125,13 @@ TEST_P(JsonSchema, TestSuite) {
   }
 }
 
-INSTANTIATE_TEST_SUITE_P(Draft3, JsonSchema, SchemaTests(Version::Draft03), SchemaTestName);
-INSTANTIATE_TEST_SUITE_P(Draft4, JsonSchema, SchemaTests(Version::Draft04), SchemaTestName);
-INSTANTIATE_TEST_SUITE_P(Draft6, JsonSchema, SchemaTests(Version::Draft06), SchemaTestName);
-INSTANTIATE_TEST_SUITE_P(Draft7, JsonSchema, SchemaTests(Version::Draft07), SchemaTestName);
-INSTANTIATE_TEST_SUITE_P(Draft2019_09, JsonSchema, SchemaTests(Version::Draft2019_09),
+INSTANTIATE_TEST_SUITE_P(Draft3, JsonSchemaTest, SchemaTests(Version::Draft03), SchemaTestName);
+INSTANTIATE_TEST_SUITE_P(Draft4, JsonSchemaTest, SchemaTests(Version::Draft04), SchemaTestName);
+INSTANTIATE_TEST_SUITE_P(Draft6, JsonSchemaTest, SchemaTests(Version::Draft06), SchemaTestName);
+INSTANTIATE_TEST_SUITE_P(Draft7, JsonSchemaTest, SchemaTests(Version::Draft07), SchemaTestName);
+INSTANTIATE_TEST_SUITE_P(Draft2019_09, JsonSchemaTest, SchemaTests(Version::Draft2019_09),
                          SchemaTestName);
-INSTANTIATE_TEST_SUITE_P(Draft2020_12, JsonSchema, SchemaTests(Version::Draft2020_12),
+INSTANTIATE_TEST_SUITE_P(Draft2020_12, JsonSchemaTest, SchemaTests(Version::Draft2020_12),
                          SchemaTestName);
 
 int main(int argc, char ** argv) {

+ 135 - 0
tests/validation_visitor_test.cxx

@@ -0,0 +1,135 @@
+#include <jvalidate/validation_visitor.h>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <json/value.h>
+
+#include <jvalidate/adapters/jsoncpp.h>
+#include <jvalidate/constraint/general_constraint.h>
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/regex.h>
+#include <jvalidate/schema.h>
+#include <jvalidate/validator.h>
+
+#include "matchers.h"
+
+using enum jvalidate::schema::Version;
+using jvalidate::Status;
+using enum jvalidate::adapter::Type;
+using jvalidate::adapter::JsonCppAdapter;
+
+using testing::Eq;
+
+namespace jvalidate {
+class ValidationVisitorTest : public testing::Test {
+protected:
+  template <typename JSON = Json::Value const>
+  auto visit(jvalidate::detail::Pointer ptr, auto const & cons, JSON & json,
+             jvalidate::ValidationResult * result = nullptr, bool annotate_everything = false) {
+    JsonCppAdapter const adapter(json);
+    ValidationVisitor visitor(node_, adapter, cfg_, regex_, extension_, result);
+    if (annotate_everything) {
+      visitor.tracking_ = StoreResults::ForAnything;
+    }
+    visitor.schema_path_ = ptr;
+    return visitor.visit(cons, adapter);
+  }
+
+  template <typename JSON = Json::Value const>
+  auto visit(jvalidate::Not<jvalidate::detail::Pointer> auto const & cons, JSON & json,
+             jvalidate::ValidationResult * result = nullptr, bool annotate_everything = false) {
+    return visit({}, cons, json, result, annotate_everything);
+  }
+
+  void config(jvalidate::ValidationConfig cfg) { cfg_ = cfg; }
+
+private:
+  jvalidate::schema::Node node_;
+  jvalidate::StdRegexEngine regex_;
+  jvalidate::ValidationConfig cfg_;
+  jvalidate::detail::StubExtensionVisitor extension_;
+};
+
+TEST_F(ValidationVisitorTest, StubExtensionIsNoop) {
+  constraint::ExtensionConstraint cons;
+  EXPECT_THAT(visit(cons, {}), Eq(Status::Noop));
+}
+
+TEST_F(ValidationVisitorTest, StubExtensionAnnotates) {
+  constraint::ExtensionConstraint cons;
+  ValidationResult result;
+  visit("/extension"_jptr, cons, {}, &result);
+  EXPECT_THAT(result, AnnotationAt("extension", "unsupported extension"));
+}
+
+TEST_F(ValidationVisitorTest, TypeMatchesType) {
+  constraint::TypeConstraint cons{{Integer}};
+  EXPECT_THAT(visit(cons, "5"_json), Eq(Status::Accept));
+}
+
+TEST_F(ValidationVisitorTest, TypeNumberMatchesInteger) {
+  constraint::TypeConstraint cons{{Number}};
+  EXPECT_THAT(visit(cons, "5"_json), Eq(Status::Accept));
+}
+
+TEST_F(ValidationVisitorTest, TypeIntegerMatchesWholeDecimal) {
+  constraint::TypeConstraint cons{{Integer}};
+  // JsonCppAdapter follows the convention of implicitly treating whole doubles
+  // that fit in int64s as integer type, but the specification allows for much
+  // wider integers than that as far as TypeConstraint is concerned.
+  EXPECT_THAT(visit(cons, "10000000000000000000.0"_json), Eq(Status::Accept));
+}
+
+TEST_F(ValidationVisitorTest, TypeIntegerDoesNotMatchFractional) {
+  constraint::TypeConstraint cons{{Integer}};
+  EXPECT_THAT(visit(cons, "5.2"_json), Eq(Status::Reject));
+}
+
+TEST_F(ValidationVisitorTest, ConstConstraintCanPerformStrictOrLooseEquality) {
+  constraint::ConstConstraint cons{JsonCppAdapter("\"true\""_json).freeze()};
+
+  config({.strict_equality = false});
+  EXPECT_THAT(visit(cons, "true"_json), Eq(Status::Accept));
+
+  config({.strict_equality = true});
+  EXPECT_THAT(visit(cons, "true"_json), Eq(Status::Reject));
+}
+
+TEST_F(ValidationVisitorTest, EnumConstraintCanPerformStrictOrLooseEquality) {
+  constraint::EnumConstraint cons;
+  cons.enumeration.push_back(JsonCppAdapter("\"true\""_json).freeze());
+
+  config({.strict_equality = false});
+  EXPECT_THAT(visit(cons, "true"_json), Eq(Status::Accept));
+
+  config({.strict_equality = true});
+  EXPECT_THAT(visit(cons, "true"_json), Eq(Status::Reject));
+}
+
+TEST_F(ValidationVisitorTest, EnumConstraintAnnotatesMatchingIndex) {
+  constraint::EnumConstraint cons;
+  cons.enumeration.push_back(JsonCppAdapter("\"true\""_json).freeze());
+  cons.enumeration.push_back(JsonCppAdapter("true"_json).freeze());
+
+  ValidationResult result;
+  visit("/enum"_jptr, cons, "true"_json, &result, true);
+  EXPECT_THAT(result, ErrorAt("enum", "1"));
+}
+
+TEST_F(ValidationVisitorTest, UnimplementedFormatIsError) {
+  constraint::FormatConstraint cons{"bogus", true};
+  config({.validate_format = true});
+
+  ValidationResult result;
+  EXPECT_THAT(visit("/format"_jptr, cons, "\"Hello\""_json, &result), Eq(Status::Reject));
+  EXPECT_THAT(result, ErrorAt("format", "bogus is unimplemented"));
+}
+}
+
+#if !defined(JVALIDATE_MONOTEST)
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}
+#endif