. "$BST_LIB" # BST-TIMEOUT: 1800 # FOUR python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output; # # the probe itself parses documents of under 100 bytes and # # resolves about twenty references over them, which is # # nothing beside the interpreter load. # # # # 1800 RATHER THAN THE 1200 THE NEIGHBOURING PYTHON SUITES # # USE, because those cover two starts and this one covers # # four: `import jsonref', `import json' and # # `import urllib.request' are each driven on their own # # before the probe, and each is a full load of one large # # static image. The three are worth their starts -- see # # `the interpreter' below, where each names a DIFFERENT # # cause for the same symptom. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-jsonref 1.1.0 (meta-openembedded/meta-python # recipes-devtools/python-jsonref/python3-jsonref_1.1.0.bb -- `inherit pypi # python_setuptools_build_meta', PyPI sdist, carrying the recipe's # migrate-to-pdm-backend.patch) on the wasm32-hwjs glibc image -- JSON REFERENCE # DEREFERENCING. A document containing `{"$ref": "#/definitions/x"}' is loaded so # that the reference behaves like the value it points at. # # ============================================================================ # WHAT THIS PORT ADDS, AND WHY IT IS NOT `doc["definitions"]["x"]'. # ============================================================================ # # Following one `$ref' by hand is one expression, so a reader will ask what the # package is for. There are three answers, and the cases below are organised # around them. # # It is TRANSPARENT. `replace_refs' does not copy the referent into place; it # substitutes a PROXY that forwards everything -- comparison, subscripting, # iteration -- to the value it points at. So code written against a document with # no references works unchanged on one full of them, which is the only reason a # config format can use `$ref' at all. # # It is LAZY, and that is what makes RECURSION representable. A JSON Schema whose # node type contains a list of nodes is written as a `$ref' pointing at its own # ancestor. Resolved eagerly that is an infinite regress; deferred to first # access it is a finite object walkable to any depth. This is the case the # package exists for, and `recur-*' below is where it is asserted. # # It ROUND-TRIPS. `jsonref.dumps' writes every proxy back as the `$ref' object it # was built from, so a tool that reads a config and writes it out again does not # silently inline half of it. `roundtrip-*' asserts that, and `plain-dumps' # asserts the stdlib encoder cannot do it -- which is what makes the property # this package's doing rather than something that would have happened anyway. # # ============================================================================ # THE ASSERTIONS ARE MEASURED, NOT PREDICTED -- ON x86-64, NOT ON THIS ARM. # ============================================================================ # # Every literal below was measured against the sdist the recipe's SRC_URI # sha256 pins (32fe8e1d...552, verified by digest before anything was measured), # with the recipe's migrate-to-pdm-backend.patch applied and the result installed # by pip -- so the module, the dist-info and the behaviour are the ones this # recipe builds, not whatever a distro happened to have. # # THE CONTROL INTERPRETER IS CPython 3.10 AND THIS IMAGE SHIPS 3.12. That gap is # stated rather than papered over: it is why `$SITE' below is a 3.12 path that # the control run never exercised, and it is the first thing to suspect if a # behavioural case reds in a way the packaging cases do not explain. # # NO on-target result exists yet. This port's first run on wasm32-hwjs is the # batch verify that this suite is part of. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND TWO FLAT MODULES. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. # # jsonref ships NO C: the sdist holds two .py files, a LICENSE, a README and a # pyproject.toml, and pyproject's `includes' names exactly `jsonref.py' and # `proxytypes.py'. `pkg-no-so' below is therefore a REGRESSION TRIPWIRE on a # version bump rather than a live risk. # # TWO modules, and the second one is the packaging risk. jsonref.py opens with a # top-level absolute `from proxytypes import LazyProxy' -- not a relative import, # and not a vendored copy -- so `proxytypes' is a SEPARATE top-level importable # that this one package must also install, under a name generic enough to # collide with anything else that claims it. A wheel that shipped only jsonref.py # would fail at `import jsonref' outright, and `proxytypes-file' and # `proxytypes-is-base' below are what say which file answered and that jsonref # really took its proxy base from it. # # ============================================================================ # THE DEPENDENCIES, AND THE ONE THAT IS NOT OBVIOUS. # ============================================================================ # # The recipe's RDEPENDS are python3-core, python3-json and python3-netclient, all # sub-packages of the ported python3 (BRINTOS-1476), so nothing about this port # waits on another package. Its two DEPENDS -- python3-pdm-native and # python3-pdm-backend-native -- build for the HOST and put nothing on the image. # # python3-netclient is the one worth naming, because it is not obvious why a # library about `$ref' strings needs the network stack. jsonref.py imports # `urlopen' AT MODULE SCOPE, unconditionally: a reference may name another # document, and the default loader fetches it through urllib. So urllib is needed # to IMPORT the module, not merely to use a remote reference -- and python3- # netclient is what ships it (it also pulls in python3-email, which urllib.request # needs in turn). That is why `urllib-import' below is one of the three separate # interpreter starts: a missing python3-netclient presents exactly as a broken # jsonref. # # NOTHING BELOW REACHES THE NETWORK. The probe's remote-reference stages install # a loader of their own -- a plain callable returning a dict -- and the one stage # that does I/O reads a file the probe writes under TMPDIR. jsonref's own # urlopen() path is never driven with an http URI. # # ============================================================================ # THE ONE STAGE THIS PORT EXPECTS COULD FAIL HERE, STATED AS A PREDICTION. # ============================================================================ # # The `file-*' cases are the only ones that leave the interpreter: they resolve a # reference into a second document on disk, which goes through urlopen() over a # `file:' URI and so through urllib's FileHandler and this platform's open/read # on the scratch filesystem. Every other stage resolves inside one already-parsed # document. # # On the control they pass. Whether they pass HERE is NOT KNOWN -- this suite has # never run on wasm32-hwjs, and no on-target measurement of it exists. If they # red while the local, chain, recursion, laziness and error stages stay green, # the finding is about this platform's `file:' path and not about jsonref, and # the probe is structured to make exactly that distinction readable: each stage # is wrapped separately, so a `file_error' key can be the only thing that # changes. Attribute such a red to the platform, do not "fix" the package. # # This is a PREDICTION about where a red would come from, not a report of one. # The runner's first emitted line is eaten on this platform # (linux-in-the-browser#177), and the harness computes its pass count as # total-minus-failures, so THE DROPPED LINE IS COUNTED AS A PASS. A nothing-case # therefore stands in that position and the eaten line costs nothing. It sits # ahead of the `require' guards deliberately, because a `require' that skips # would emit its skip as the first line and be eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages MOD=$SITE/jsonref.py PROXY=$SITE/proxytypes.py DI=$SITE/jsonref-1.1.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-jsonref.list PROBE=$BST_DATA/python3-jsonref-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-jsonref.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import jsonref' further down is the functional # statement; these are the ones that say WHICH copy answered, so a package that # imported out of some other path is distinguishable from one that imported out # of what this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-file test -f "$MOD" # THE SECOND MODULE. See the header: jsonref.py cannot import without it, and it # claims a top-level name generic enough to collide. check $P/proxytypes-file test -f "$PROXY" # ...AND BOTH ARE FILES, NOT DIRECTORIES. A release that grew either into a # package would still import and would still pass most cases below, while the # shape this image ships had changed. check_eq $P/no-jsonref-dir "absent" \ "$(if [ -d "$SITE/jsonref" ]; then echo present; else echo absent; fi)" check_eq $P/no-proxytypes-dir "absent" \ "$(if [ -d "$SITE/proxytypes" ]; then echo present; else echo absent; fi)" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on # this image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- loudly, here, instead of at `import' later. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # THIS PACKAGE SHIPS NO EXECUTABLE, which is a real difference from its # neighbours rather than an incidental one. The sdist declares no `scripts=' and # pyproject declares no [project.scripts], so there is no entry point to # generate -- and the built python3-jsonref_1.1.0-r0_wasm32.deb confirms it, # holding nothing outside site-packages. Unlike python3-jsonpointer, which ships # /usr/bin/jsonpointer and whose rewritten shebang is its main packaging risk, # jsonref has no executable to get wrong. A file appearing under a bin directory # here would mean the packaging changed shape. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/(usr/)?s?bin/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The two cases # above name failures that are expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray README -- without having to enumerate # what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata, which is the second, independent statement of the # version -- `$P/dist-version' below reads it through importlib.metadata, and the # directory name carries it a third time in $DI itself. A package whose module # landed and whose metadata did not would import fine and serve every behavioural # case while reporting no installed version at all. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.1.0" cat "$DI/METADATA" # The distribution's NAME, asserted separately from its version: $DI carries both # in one directory name, so a case on the directory alone cannot say which half # was wrong. check_out $P/dist-info-name "Name: jsonref" cat "$DI/METADATA" # # NO top_level.txt CASE HERE, DELIBERATELY. 99-python3-jsonpointer asserts one, # because setuptools generates it. This package is built by pdm-backend (the # recipe's migrate-to-pdm-backend.patch selects it), which does NOT -- measured # on the built .deb, whose dist-info holds exactly METADATA, RECORD, WHEEL and # licenses/LICENSE. Asserting top_level.txt's ABSENCE would be pinning which # build backend the recipe uses, which is not this port's business; # `import-name' below states the importable name where it is decidable. # -------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-jsonref is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take a -ptest or -dev # stanza of the same family. sed -n '/^Package: python3-jsonref$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. All three # are sub-packages of the ported python3 (BRINTOS-1476), which is why this port # waits on nothing. netclient is the one that is not obvious -- see the header: # jsonref imports urlopen at module scope, so urllib is needed to IMPORT it. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-json "python3-json" cat "$W/dep" check_out $P/pkg-depends-netclient "python3-netclient" cat "$W/dep" # ---------------------------------------------------------- the interpreter --- # THE IMPORTS, ON THEIR OWN, BEFORE THE PROBE. If jsonref cannot be imported then # every probe case below reds at once and each names a different symptom; these # three are what say the cause is the import, and WHICH import. # # They are three separate interpreter starts on purpose, and the BST-TIMEOUT # above is sized for them. Each names a different cause for the same symptom: check $P/import python3 -c 'import jsonref' # ...`json' separately, because jsonref is useless without the parser whose # output it walks, and a missing python3-json presents AS a broken jsonref. check $P/json-import python3 -c 'import json' # ...and `urllib.request' separately, because jsonref imports urlopen at MODULE # SCOPE. A missing python3-netclient does not degrade remote references, it makes # `import jsonref' fail outright -- and that is indistinguishable from a broken # package until this case tells the two apart. check $P/urllib-import python3 -c 'import urllib.request' # The importable name, stated where it is decidable -- see the dist-info note on # why there is no top_level.txt case. A wheel that renamed the module would fail # `import' above; this says the name that worked is the one the package is named # for, imported as a TOP-LEVEL module rather than out of some package. check_out $P/import-name "jsonref" python3 -c 'import jsonref; print(jsonref.__name__)' # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `version' distinguishable # from `dist_version', and `local_value' from `local_type'. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE VERSION THE RECIPE PINS, twice and independently: the module's own # `__version__' (which pdm reads out of jsonref.py to set the package version) # and importlib.metadata over the installed dist-info. The two disagreeing means # a mixed install -- a dist-info from one version beside a module from another. check_eq $P/version "1.1.0" "$(pv version)" check_eq $P/dist-version "1.1.0" "$(pv dist_version)" # The modules answered out of the files this image installed, not from stray # copies on sys.path. This is what makes the payload cases above and the # behaviour cases below statements about the SAME jsonref. check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/proxytypes-answered "$PROXY" "$(pv proxytypes_file)" # ...and jsonref's proxy base really IS that module's LazyProxy, rather than a # vendored copy of its own that happens to be named the same. This is the case # that makes `proxytypes-file' load-bearing instead of incidental. check_eq $P/proxytypes-lazyproxy "yes" "$(pv proxytypes_lazyproxy)" check_eq $P/proxytypes-is-base "yes" "$(pv proxytypes_is_base)" # THE PUBLIC NAMES, one case each. This module declares NO __all__, so there is # no whole-surface string to assert -- dir() would report its own imports # (`json', `functools', `urlparse', `warnings', the `requests' probe) and a case # over it would be pinning implementation detail. See the probe's header. for _n in replace_refs load loads load_uri dump dumps jsonloader \ JsonRef JsonRefError URIDict; do check_eq $P/api-$_n "yes" "$(pv has_$_n)" done # The exception's BASE, which is what a caller's `except' clause names. A release # re-basing it on ValueError would leave every case below passing while breaking # every caller that catches it -- including `miss-class' here, which catches the # class by name and would not notice. check_eq $P/exc-base "Exception" "$(pv exc_base)" # WHICH FETCH PATH THE DEFAULT LOADER TAKES. jsonref tries `import requests' at # module scope and falls back to urllib, recording the answer in a module global. # This image installs no requests, so the urllib fallback is the live path. A # future image that grew requests would change how every remote reference is # fetched without changing one line of this package, and this case is where that # would show. check_eq $P/requests-backend "none" "$(pv requests_backend)" # ------------------------------------------------------------- the fixture --- # The document the local-reference stage drove, pinned by length and digest, so # every case below is known to be about the document described here rather than # about an edited probe. check_eq $P/fixture-len "92" "$(pv local_fixture_len)" check_eq $P/fixture-sha "584f3a763ad981d5" "$(pv local_fixture_sha)" # ------------------------------------- A LOCAL REFERENCE, AND THE ROUND TRIP --- # `{"$ref": "#/definitions/widget"}' resolved against the same document. # # IT IS A PROXY, not the value copied into place. That distinction is the whole # package: a copy would serialise as the value and would not stay in step with a # document still being edited. check_eq $P/local-type "JsonRef" "$(pv local_type)" # ...and the proxy COMPARES EQUAL to what it points at, which is what makes it # usable by code that does not know a reference was ever there. `local-type' and # this case together are the transparency claim: it is not the value, and it # behaves like the value. check_eq $P/local-equal "yes" "$(pv local_equal)" # Subscripting THROUGH the proxy reaches the referent's own members. check_eq $P/local-kind "widget" "$(pv local_kind)" check_eq $P/local-size "3" "$(pv local_size)" check_eq $P/local-value '{"kind":"widget","size":3}' "$(pv local_value)" # THE ROUND TRIP. jsonref.dumps restores every proxy to the `$ref' object it was # built from, so a document read, walked and written back is byte-identical to # what was read. This is the property that lets a tool that only meant to READ a # config not silently inline half of it. # # Single quotes: the expectation contains `$ref', which the shell would expand. check_eq $P/roundtrip \ '{"definitions":{"widget":{"kind":"widget","size":3}},"here":{"$ref":"#/definitions/widget"}}' \ "$(pv roundtrip)" # ...stated once more as an identity against the input, so a red here is readable # without diffing two long literals by eye. check_eq $P/roundtrip-identical "yes" "$(pv roundtrip_identical)" # AND THE STDLIB ENCODER CANNOT DO IT. json.dumps meets a proxy it has no rule # for and raises TypeError. This is the case that makes the round trip above # jsonref's doing rather than something that would have happened anyway. check_eq $P/plain-dumps "TypeError" "$(pv plain_dumps)" # `loads' is the drop-in replacement for json.loads -- parse and replace in one # call, which is the entry point a caller actually uses. It reaches the replacer # by a different path than replace_refs() above, so it gets its own cases. check_eq $P/loads-kind "widget" "$(pv loads_kind)" check_eq $P/loads-type "JsonRef" "$(pv loads_type)" # ------------------------------------------------ A CHAIN OF THREE REFERENCES --- # `a' -> `b' -> `c' -> the value. Resolving `a' has to follow all three, each hop # being a proxy that resolves during the previous one's resolution. A library # that dereferenced ONE level would return a `$ref' object here rather than the # string, and `chain-value' is what catches that. check_eq $P/chain-value '"terminal"' "$(pv chain_value)" check_eq $P/chain-equal "yes" "$(pv chain_equal)" # Entered from the middle it still reaches the end, which says the chain is # followed from wherever it is entered rather than only from its head. check_eq $P/chain-mid '"terminal"' "$(pv chain_mid)" # ------------------------------- A `file:' REFERENCE INTO A SCRATCH TREE --- # THE ONLY CASES HERE THAT LEAVE THE INTERPRETER, and the ones this port predicts # could fail on this platform. See the header: a red in this block while the # stages either side stay green is a finding about this platform's `file:' path, # not about jsonref. NOT YET MEASURED ON TARGET. # # The fixture is written by the probe under TMPDIR and removed after; base_uri is # that directory WITH a trailing slash, because urljoin replaces the last path # segment without one. check_eq $P/file-fixture-written "yes" "$(pv file_fixture_written)" check_eq $P/file-base-uri-shape "dir-with-slash" "$(pv file_base_uri_shape)" # A reference naming both another DOCUMENT and a POINTER within it; both halves # have to work. check_eq $P/file-colour '"green"' "$(pv file_colour)" # ...and an ARRAY INDEX inside that fragment, which is the token type a pointer # resolver treats differently from a mapping key. check_eq $P/file-deep '20' "$(pv file_deep)" # A reference with NO fragment is the whole of the other document. check_eq $P/file-whole '{"colour":"green","nested":{"deep":[10,20,30]}}' \ "$(pv file_whole)" # The round trip holds ACROSS FILES too: what is written back is the relative # reference, not the other file's contents inlined. check_eq $P/file-roundtrip \ '{"colour":{"$ref":"other.json#/colour"},"deep":{"$ref":"other.json#/nested/deep/1"},"whole":{"$ref":"other.json"}}' \ "$(pv file_roundtrip)" # ------------------ THE CASE THE PACKAGE EXISTS FOR: A SELF-REFERENTIAL DOCUMENT --- # `{"$ref": "#"}' addresses the whole document, and it appears INSIDE that # document -- a node whose children are nodes, which is how every recursive JSON # Schema is written. # # Resolved eagerly this is an infinite regress: the resolver substitutes the # document into itself until the stack ends. Resolved lazily it is a finite # object that can be walked to any depth on demand. So the assertions are that it # LOADS at all, and then that it can be WALKED. check_eq $P/recur-loaded "yes" "$(pv recur_loaded)" check_eq $P/recur-top-name '"node"' "$(pv recur_top_name)" check_eq $P/recur-one-level '"node"' "$(pv recur_one_level)" # The SECOND level is the load-bearing one: one hop could be a single lucky # dereference, two says the proxy is re-entrant -- the document reached through # the reference is itself full of live references. check_eq $P/recur-two-levels '"node"' "$(pv recur_two_levels)" # ...and it still writes back as `{"$ref": "#"}' rather than as whatever finite # prefix happened to be walked, which is what makes a recursive schema editable # rather than merely readable. check_eq $P/recur-roundtrip \ '{"children":{"items":{"$ref":"#"},"type":"array"},"name":"node"}' \ "$(pv recur_roundtrip)" # ------------------------------------------- LAZINESS, MEASURED NOT ASSERTED --- # The recursion cases show a self-referential document surviving, which is # EVIDENCE that resolution is deferred but not a MEASUREMENT of it. A counting # loader is: it records every call, so the number of loads before and after the # first access is the whole statement -- and it needs neither network nor # filesystem, which is why these cases are trustworthy even if `file-*' reds. # # Nothing is fetched at load time. This is the case that would red if a release # resolved eagerly, and it is the reason a recursive schema can be loaded at all. check_eq $P/lazy-calls-after-load "0" "$(pv lazy_calls_after_load)" check_eq $P/lazy-value '"fetched"' "$(pv lazy_value)" # ...and exactly one fetch happens when the value is finally asked for. check_eq $P/lazy-calls-after-access "1" "$(pv lazy_calls_after_access)" # The URI the loader was handed, which says the relative reference was resolved # against base_uri rather than passed through raw. check_eq $P/lazy-uri "file:///base/remote.json" "$(pv lazy_uri_requested)" # A SECOND, separate `$ref' to the same URI does not fetch again: the store is # shared across every reference in one replace_refs call. check_eq $P/lazy-calls-shared-store "1" "$(pv lazy_calls_shared_store)" # ...and re-reading the same reference does not fetch again either: LazyProxy # caches on first use. Without this, a document walked twice would fetch twice. check_eq $P/lazy-calls-reaccess "1" "$(pv lazy_calls_reaccess)" # THE CONTRAST, which is what says laziness is a CHOICE this library makes rather # than an accident of how the fixture was built. Same document, same shape of # loader, both documented opt-outs -- and each fetches immediately. check_eq $P/eager-calls "1" "$(pv eager_calls_after_load)" check_eq $P/noproxy-calls "1" "$(pv noproxy_calls_after_load)" # ...and proxies=False leaves NO proxy behind at all, which is the shape a caller # wants when the document is about to be handed to something that type-checks. check_eq $P/noproxy-type "dict" "$(pv noproxy_type)" # --------------------------------- A REFERENCE THAT NAMES NOTHING --- # WHERE it raises is as much the contract as THAT it raises. Loading a document # whose reference is unresolvable must SUCCEED -- otherwise a schema with one bad # `$ref' in a branch nobody touches could not be opened at all -- and the error # has to arrive when that branch is actually read. check_eq $P/miss-load "ok" "$(pv miss_load)" # At load time it is still an ordinary proxy: nothing has been resolved yet, so # nothing has failed yet. check_eq $P/miss-type-before-access "JsonRef" "$(pv miss_type_before_access)" # ...and the REST of the document is usable, which is the practical half of # deferring the error. check_eq $P/miss-sibling-readable '"fine"' "$(pv miss_sibling_readable)" # Then, on access, it raises -- and raises the library's OWN class, not a bare # KeyError leaking from the lookup underneath. check_eq $P/miss-raised "yes" "$(pv miss_raised)" check_eq $P/miss-class "JsonRefError" "$(pv miss_class)" # The message names the full reference and the pointer that failed. It # interpolates NO document repr -- unlike jsonpointer's member-not-found message, # which is why 99-python3-jsonpointer does not assert that one -- so asserting it # here does not pin dict ordering or the fixture's shape into a case about a # lookup. # # The expectation carries BOTH backticks and single quotes, so it is built with # the '\'' idiom rather than double-quoted, where the backticks would be command # substitution. _miss_want='Error while resolving `#/definitions/nope`: Unresolvable JSON pointer: '\''/definitions/nope'\' check_eq $P/miss-message "$_miss_want" "$(pv miss_message)" # The original reference object is kept ON the exception, which is what lets a # caller report WHICH `$ref' in its input was wrong rather than only that one was. check_eq $P/miss-reference '{"$ref":"#/definitions/nope"}' "$(pv miss_reference)" # ...and the underlying lookup failure is CHAINED rather than swallowed, so the # original cause survives for a caller that wants it. check_eq $P/miss-cause "KeyError" "$(pv miss_cause)" # The probe reached its end. Every case above reads a key the probe emits before # this one, so a probe that died midway would red them individually with got:''; # this is the single case that says the run was complete. check_eq $P/probe-complete "yes" "$(pv probe_complete)" rm -rf "$W"