. "$BST_LIB" # valijson is a header-only JSON Schema validator (draft-04 and draft-07) that # brings NO JSON parser of its own. It validates whatever DOM the caller already # has, reaching it through an ADAPTER -- one small header per supported library, # eleven of them shipped here -- so the same schema and the same validator work # over nlohmann-json, rapidjson, jsoncpp, picojson, json11, Poco, Qt, yaml-cpp, # boost.json, boost property_tree, or a bare std::string. On this image the # adapters that resolve are nlohmann-json (BRINTOS-167) and rapidjson # (BRINTOS-1113); the other nine ship as headers over libraries the image does # not carry, which is upstream's shape and is left alone. # # The port is 46 headers plus the find_package(valijson) cmake package -- ALL of # it -dev. Nothing of valijson is compiled: its cmake target is an INTERFACE # library, so the main package is empty and there is no archive, no shared object # and no CLI. Two things therefore have to be asserted here that a file listing # cannot reach on its own, and they are what most of this suite is: # # THE VALIDATOR MUST ACTUALLY REFUSE. This is the package whose entire purpose # is to say no, which makes it the package for which "the files are present" # is furthest from "it works": a validator that accepts every document passes # every existence check ever written and every smoke test that only feeds it # valid input. Section 4 runs the probe the recipe builds, whose centre is # thirteen documents that must FAIL -- one per constraint keyword -- each # compared against an x86-64 reference for the error count, the context path # and the byte-exact description. A validator reporting the WRONG constraint # is as broken as one reporting none, and only the description can tell them # apart. # THE SHIPPED CMAKE PACKAGE MUST NOT CARRY VALIJSON_USE_EXCEPTIONS=1. That one # line is the difference between a usable port and an unusable one on this # arm, and it is invisible in the headers -- see section 3, which is the # assertion this suite exists for as much as the probe is. # # THE EXCEPTIONS STORY, because a consumer on this image has to know it and # nothing in the payload says it. valijson routes its own internal errors through # throwRuntimeError(), which is `throw std::runtime_error` when # VALIJSON_USE_EXCEPTIONS is 1 and `std::cerr << msg; abort()` when it is absent # or 0. This arm has no unwinder and compiles -fno-exceptions, so the port is # configured -Dvalijson_USE_EXCEPTIONS=FALSE and the abort() spelling is what # ships. Three consequences, in the order they will bite: # # * VALIDATION FAILURE NEVER GOES NEAR IT. A document that violates the schema # is reported by value, through ValidationResults, and is the ordinary case. # That is the whole reason this package ports to an arm with no exceptions, # and it is why section 4 is assertable at all. # * A MALFORMED DOCUMENT IS REPORTABLE. valijson::utils::loadDocument() calls # nlohmann::json::parse(s, nullptr, false) under !VALIJSON_USE_EXCEPTIONS and # tests is_discarded(), so it returns false rather than dying. Probe cases # nlohmann-discarded and loaddocument-missing pin that branch as live. # * A MALFORMED SCHEMA TAKES THE PROCESS DOWN. SchemaParser::populateSchema # calls throwRuntimeError() for a schema it cannot make sense of, and that is # abort() here. There is no way to catch it and no way for this suite to # provoke it -- an aborting case would take the whole file's remaining cases # with it. A consumer on this image validates its schema at BUILD time, not # at run time. Recorded rather than tested, deliberately. # # THE ERROR COUNTS ARE A CASCADE, not a count of violations, and the probe pins # them because nothing in the API says so. valijson pushes the leaf error and # then one "Failed to validate against schema associated with property name 'X'." # for every ancestor the descent came through -- so one bad top-level property is # TWO errors, a bad array element is THREE, and two bad properties are FOUR. A # consumer printing numErrors() as "you have N problems" is wrong for every # document there is. # # Fork discipline (the lesson 81-cjson paid for): the runner's per-suite bound is # a budget in PROCESS SPAWNS, not in cases -- a check_out per case is a command # substitution plus an exec. So each file is read ONCE into a shell variable and # every text assertion is a fork-free `case` glob against it, and the probe is # run ONCE with its whole output kept. `has`/`hasfile`/`hasline` stay local to # this suite so a hot helper cannot change behaviour under the other suites. inc=/usr/include/valijson compat=/usr/include/compat/optional.hpp cm=/usr/lib/cmake/valijson/valijsonConfig.cmake PT=/usr/libexec/valijson-tests nl=' ' # hasfile -- ok iff is a non-empty regular file. # has -- ok iff contains the literal . # hasline -- ok iff has as a WHOLE line (a # substring test would let internal/optional.hpp # satisfy a lookup for optional.hpp). # lacks -- ok iff does NOT contain . hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "missing: $2" ;; esac; } lacks() { case "$3" in *"$2"*) bst_notok "$1" "present but must not be: $2" ;; *) bst_ok "$1" ;; esac; } # --------------------------------------------------------------------------- # 1. The shipped header tree. # # One find for the whole inventory; every presence case below is a glob against # it rather than its own stat. files=$(find "$inc" -name '*.hpp' 2>/dev/null | sed "s|^$inc/||" | sort) nfiles=$(printf '%s\n' "$files" | grep -c .) # 45 under valijson/ is the full 1.0.2 install. The floor guards the checks that # follow: an empty find would otherwise let the closure check pass vacuously. if [ "$nfiles" -ge 45 ]; then bst_ok valijson/tree-count else bst_notok valijson/tree-count "installed=$nfiles want>=45"; fi # The six headers a consumer names directly. schema + schema_parser build the # schema, validator + validation_results run it and report, subschema is the node # type a custom constraint sees, and exceptions.hpp is the one whose contents are # selected by the cmake configuration asserted in section 3. for f in schema.hpp schema_parser.hpp validator.hpp validation_results.hpp \ subschema.hpp exceptions.hpp; do hasline "valijson/hdr-${f%.hpp}" "$f" "$files" done # adapters/: the seam. Eleven, and the port's own consumer uses exactly one of # them -- but a packaging split that dropped the others would break every OTHER # consumer silently, since a missing adapter is a compile error in the caller's # translation unit and not here. for f in nlohmann_json_adapter.hpp rapidjson_adapter.hpp jsoncpp_adapter.hpp \ picojson_adapter.hpp json11_adapter.hpp std_string_adapter.hpp \ boost_json_adapter.hpp poco_json_adapter.hpp qtjson_adapter.hpp \ yaml_cpp_adapter.hpp property_tree_adapter.hpp; do hasline "valijson/adapter-${f%_adapter.hpp}" "adapters/$f" "$files" done nadapters=$(printf '%s\n' "$files" | grep -c '^adapters/') if [ "$nadapters" -eq 11 ]; then bst_ok valijson/adapter-count else bst_notok valijson/adapter-count "adapters=$nadapters want=11"; fi # utils/: the loadDocument() helper per adapter, plus the file reader and the # UTF-8 helper they share. nlohmann_json_utils.hpp is the one the probe calls, # and it is where the exceptions branch is chosen. for f in file_utils.hpp nlohmann_json_utils.hpp rapidjson_utils.hpp \ utf8_utils.hpp; do hasline "valijson/util-${f%.hpp}" "utils/$f" "$files" done # constraints/ + internal/: the implementation the facade is assembled from. Not # private in any packaging sense -- schema_parser.hpp includes all of it -- so a # file missing here breaks the build of a consumer that only ever wrote # #include . for f in constraints/concrete_constraints.hpp constraints/constraint.hpp \ constraints/basic_constraint.hpp constraints/constraint_visitor.hpp \ internal/adapter.hpp internal/basic_adapter.hpp \ internal/json_pointer.hpp internal/json_reference.hpp \ internal/optional.hpp internal/optional_bundled.hpp \ internal/custom_allocator.hpp internal/frozen_value.hpp \ internal/uri.hpp internal/debug.hpp; do hasline "valijson/impl-$(echo "$f" | sed 's|/|-|;s|\.hpp$||')" "$f" "$files" done # THE ONE HEADER THAT IS NOT UNDER valijson/. internal/optional.hpp includes # , a vendored std::optional backport that upstream installs # at the TOP of the include path rather than under its own namespace directory. # So this port adds /usr/include/compat/ to the image, which is a name generic # enough that a future package could collide with it. Asserted because it is # load-bearing -- without it internal/optional.hpp does not compile, and the # closure check below cannot see it, since it is reached by a path that does not # start with valijson/. hasfile valijson/compat-optional "$compat" # --------------------------------------------------------------------------- # 2. The include closure -- the tree asserted against ITSELF. # # A header-only library is only usable if what it ships is SELF-CONTAINED: # validator.hpp is a facade over a dozen others, each including more, so one # header dropped by a FILES glob turns every consumer's first #include into a # compile error -- and no case in section 1 would notice, because section 1 only # knows the names someone thought to write down here. This reads every # `#include ` in the INSTALLED tree and requires each target to be # installed too. missing="" for h in $(grep -rho '#include *]*>' "$inc" 2>/dev/null | sed 's|.*.*||' | sort -u); do [ -f "$inc/$h" ] || missing="$missing $h" done if [ -z "$missing" ]; then bst_ok valijson/include-closure else bst_notok valijson/include-closure "unresolved:$missing"; fi # --------------------------------------------------------------------------- # 3. The cmake package -- the assertion this port turns on. # # valijson's whole build output is this one file. Its shape matters twice over: # # (a) IT MUST DECLARE AN INTERFACE IMPORTED TARGET. install(TARGETS valijson # EXPORT ...) names a DESTINATION for an INTERFACE library, which installs # nothing there -- so ${libdir} gains a cmake/ directory and no library, and # that reads like a packaging fault the first time it is met. It is correct, # and this case is what says so. # # (b) IT MUST NOT CARRY VALIJSON_USE_EXCEPTIONS=1, AND THAT IS THE WHOLE POINT. # Upstream's valijson_USE_EXCEPTIONS defaults TRUE, and when it is TRUE cmake # writes INTERFACE_COMPILE_DEFINITIONS "VALIJSON_USE_EXCEPTIONS=1" into this # file. Every consumer that writes find_package(valijson) then inherits it, # exceptions.hpp switches to `throw std::runtime_error`, and the consumer # fails to compile on this arm with # "exception handling disabled, use -fexceptions to enable" -- from inside a # library header, for a reason nothing in the header tree hints at. Measured # both ways on x86-64 before the port was written: with the option TRUE the # define is present and a minimal consumer will not compile under # -fno-exceptions; with it FALSE the define is absent and the same consumer # compiles clean. # # So the bbappend sets -Dvalijson_USE_EXCEPTIONS=FALSE, and this pair of # cases is the only thing standing between a silent regression of that line # and an image shipping a validator no consumer on it can use. A file listing # would not notice; the probe would not notice either, because it is compiled # from ${S}/include by the recipe and never goes through find_package. hasfile valijson/cmake-present "$cm" cmtext=$(cat "$cm" 2>/dev/null) has valijson/cmake-interface-target "add_library(valijson INTERFACE IMPORTED)" "$cmtext" has valijson/cmake-include-dirs "INTERFACE_INCLUDE_DIRECTORIES" "$cmtext" lacks valijson/cmake-no-exceptions-define "VALIJSON_USE_EXCEPTIONS" "$cmtext" lacks valijson/cmake-no-compile-definitions "INTERFACE_COMPILE_DEFINITIONS" "$cmtext" # The prefix the config computes must be the target's, not the build host's. A # cmake package naming a path inside the build tree is useless on target and is # exactly what an install-tree/build-tree mix-up produces. lacks valijson/cmake-no-buildpath "/tmp-glibc/" "$cmtext" has valijson/cmake-install-prefix "/usr/lib/cmake/valijson" "$cmtext" # --------------------------------------------------------------------------- # 4. The probe -- the library actually running. # # Built by the valijson recipe from the very headers that ship, in the same # build, against the nlohmann-json headers the image carries -- so it cannot # drift from what it tests, and it is the first translation unit on this arm to # compile EITHER library. Installed 0755 in libexecdir and needing no staging: it # opens its two fixtures read-only and writes nothing, so the read-only rootfs at # browser runtime is not in its way. `require' rather than a hard failure -- an # image that takes valijson-dev without valijson-tests legitimately has nothing # to run here, and that is a coverage prompt (add the package), not a broken # port. require "$PT/valijson-probe" valijson/probe-present hasfile valijson/schema-present "$PT/valijson-schema.json" hasfile valijson/doc-present "$PT/valijson-doc.json" # Run ONCE and keep both the output and the status: on this runtime the wasm # module's start-up dominates the cost of the whole suite. out=$("$PT/valijson-probe" "$PT" 2>&1); prc=$? bst_ok valijson/probe-ran # the fixtures, loaded from file through valijson's own helper, and the schema # parsed into valijson's internal form has valijson/run-schema-load "VJ-schema-load OK" "$out" has valijson/run-doc-load "VJ-doc-load OK" "$out" has valijson/run-schema-is-object "VJ-schema-is-object OK" "$out" has valijson/run-schema-required "VJ-schema-required-3 OK" "$out" has valijson/run-schema-properties "VJ-schema-properties-6 OK" "$out" has valijson/run-schema-parse "VJ-schema-parse OK" "$out" # the document that must PASS -- including with no results sink, and twice over, # since the adapter is a view and a validator that mutated the caller's DOM would # be invisible to a single-shot test has valijson/run-valid-accepted "VJ-valid-accepted OK" "$out" has valijson/run-valid-no-errors "VJ-valid-no-errors OK" "$out" has valijson/run-valid-no-sink "VJ-valid-no-sink OK" "$out" has valijson/run-valid-idempotent "VJ-valid-idempotent OK" "$out" # THE CENTRE OF THE SUITE: thirteen documents that must FAIL, one per constraint # keyword. Each pins the error COUNT (the cascade), the context PATH of the leaf, # its byte-exact DESCRIPTION, and the description of the cascade's tail. A # validator that refused for the wrong reason, or that stopped at the first # violation, or that lost the ancestor chain, fails one of these and not the # others -- which is what makes the group a diagnosis rather than a verdict. # # bad-minimum and bad-maximum are also this port's only TARGET measurement: the # bound is held as a double and rendered with %f, so the reference reads # "1024.000000" and not "1024". They are where a wasm32 double-to-string # divergence would surface; every other case here would still pass with a # numeric formatter that was merely close. for c in bad-type bad-required bad-pattern bad-minimum bad-maximum \ bad-minlength bad-enum bad-minitems bad-uniqueitems bad-items \ bad-additional bad-multipleof bad-two; do has "valijson/run-$c" "VJ-$c OK" "$out" done # kStrongTypes vs kWeakTypes -- the same document, the opposite verdict. The one # valijson setting a consumer must choose rather than accept: weak types let the # string "4242" satisfy "type":"integer", which is what a caller reading a query # string wants and what a caller validating an API payload must not have. The # two "still refuses" cases are what keep this from being satisfied by a # validator that simply stopped checking under weak types. has valijson/run-types-strong "VJ-types-strong-refuses OK" "$out" has valijson/run-types-weak "VJ-types-weak-accepts OK" "$out" has valijson/run-types-weak-refuses "VJ-types-weak-still-refuses OK" "$out" has valijson/run-types-weak-bounds "VJ-types-weak-keeps-bounds OK" "$out" # the ValidationResults contract: FIFO order, non-destructive iteration, and the # leaf/ancestor interleaving of the cascade has valijson/run-results-count "VJ-results-count OK" "$out" has valijson/run-results-iterate "VJ-results-iterate OK" "$out" has valijson/run-results-nondestructive "VJ-results-iterate-nondestructive OK" "$out" has valijson/run-results-fifo "VJ-results-fifo-order OK" "$out" has valijson/run-results-leaves "VJ-results-leaf-descriptions OK" "$out" has valijson/run-results-ancestors "VJ-results-ancestor-descriptions OK" "$out" has valijson/run-results-drained "VJ-results-drained OK" "$out" has valijson/run-results-context "VJ-results-context-depth OK" "$out" # the exceptions-off contract, proved at run time on the arm rather than argued # from the recipe: nlohmann's discarded-value path is live, loadDocument reports # an absent file instead of aborting, and the binary was built without the define has valijson/run-nlohmann-discarded "VJ-nlohmann-discarded OK" "$out" has valijson/run-nlohmann-ok "VJ-nlohmann-not-discarded OK" "$out" has valijson/run-loaddocument-missing "VJ-loaddocument-missing OK" "$out" has valijson/run-exceptions-off "VJ-exceptions-off OK" "$out" # the adapter itself -- valijson never touches nlohmann's API directly, so these # predicates ARE the seam and are asserted rather than only inferred from a # verdict that could pass for the wrong reason for c in adapter-isobject adapter-notarray adapter-isnumber adapter-asinteger \ adapter-isstring adapter-asstring adapter-isarray adapter-arraysize \ adapter-isdouble adapter-asdouble adapter-integer-is-number; do has "valijson/run-$c" "VJ-$c OK" "$out" done # THE PROBE'S OWN DIAGNOSIS, WHICH IS OTHERWISE LOST. Every case above is a `has` # against $out and reports only "not found: VJ-x OK" -- but the probe prints # `VJ-x FAIL: ` beside each failure, carrying the actual count, # path or description, and that text never reaches the console because $out is a # command substitution. A reader of the run would then see reds with no reason # for any of them and have to rebuild and re-boot to learn what the values were. # Re-emit each FAIL line as its own case carrying the detail: nothing is added on # a green run, and a red one explains itself the first time. IFS="$nl" for l in $out; do case "$l" in VJ-*\ FAIL:*) c=${l#VJ-}; c=${c%% *} bst_notok "valijson/probe-$c" "$(_bst_1line "${l#*FAIL: }")" ;; esac done unset IFS # THE COUNT, which is the guard the per-case assertions cannot be. Every `has` # above can only see what the probe printed, so a probe that silently stopped # emitting half its cases would satisfy all of them and still reach # ALL-VALIJSON-OK -- main() prints that whenever its failure counter is zero, and # a group skipped without failing leaves the counter zero. Pinning the number is # what notices. A case ADDED to the probe reddens this one, which is the intent: # update the number in the same change. cases=$(printf '%s\n' "$out" | grep -c '^VJ-') check_eq valijson/probe-case-count "50" "$cases" # The probe's own verdict, which also catches a case this list forgot to name: it # counts its OWN failures, so a case added to the probe and not here still turns # the suite red rather than passing unnoticed. has valijson/run-all-ok "ALL-VALIJSON-OK" "$out" if [ "$prc" = 0 ]; then bst_ok valijson/run-exit-status else bst_notok valijson/run-exit-status "probe exited $prc: $(_bst_1line "$out")" fi # The last line of the suite, and a coverage assertion rather than a formality: a # suite that dies mid-file loses its remaining cases SILENTLY -- the runner counts # the lines it saw and reports them as a complete green -- so the only way a # reader can tell a full run from a truncated one is a marker that only a full run # emits. Its absence is what a regression here looks like; see the pass criterion # in docs/brintos-test-plan.md. bst_ok valijson/suite-complete