. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. A bare # # `import', a second one whose STDERR is the assertion # # (`import-quiet'), and the probe, which takes ONE start for # # a hundred and seventy measurements. # # # # Everything else in this file is a shell builtin over a # # tmpfs copy of a dpkg list, or a grep of the probe's # # output. The work inside each start is a Pratt parse and a # # tree walk over a fixture document of a few hundred bytes # # -- microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line of the # # program runs. # # # # 1200 is 400s per start, the same budget 99-python3-wcwidth # # and 99-python3-webcolors set for their three, and roughly # # seven times the ~60s a python3 start has measured at here. # # There is no loop in this file and none in the probe. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than this on target the RIGHT fix is # # this number, not fewer cases. # # # # 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-jmespath 1.0.1 (meta-openembedded meta-python # recipes-devtools/python/python3-jmespath_1.0.1.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS: A LEXER, A PRATT PARSER AND A TREE WALKER. # ============================================================================ # # JMESPath is a query language for JSON. An expression names a path through a # document; jmespath compiles it to an AST of plain dicts and walks that AST # over the data, returning the selected value. Eight modules, 1668 lines, and no # C at all -- the whole implementation is `lexer.py' (a hand-written scanner), # `parser.py' (a Pratt parser with a binding-power table), `visitor.py' (the # tree walk) and `functions.py' (the specification's function library, built # into a table by a metaclass at class-creation time). # # What a red here is most likely about is one of three things: the FUNCTION # TABLE (if the metaclass did not run, every function name is unknown at once); # the TRUTHINESS RULES, which are not Python's; or the BINDING-POWER TABLE, # which has one entry a reader will not expect. All three have their own group # below. # # ============================================================================ # THE TRUTHINESS RULES ARE NOT PYTHON'S, AND THAT IS THE POINT OF THE GROUP. # ============================================================================ # # The specification calls exactly five values false: the empty array, the empty # object, the empty string, `null' and `false'. `0' IS NOT AMONG THEM. # `visitor.py' implements this as `_is_false', written with `==' comparisons # rather than `bool()' for exactly this reason -- an implementation that used # the host language's truthiness would agree on all five and differ on zero # alone. `truth-or-zero' is therefore the load-bearing case of that group and # the four that precede it are the foils; a port that got zero wrong would pass # every one of them. # # The same split shows in equality. Python's `1 == True' is true and JMESPath's # is not, so `_is_special_number_case' exists to make `` `1` == `true` `` false. # `truth-one-eq-true' and `truth-zero-eq-false' are the whole of that function's # observable effect. # # ============================================================================ # `!a.b' DOES NOT MEAN `!(a.b)'. THIS IS THE PORT'S SHARPEST FINDING. # ============================================================================ # # `parser.py's BINDING_POWER table puts `not' at 45 and `dot' at 40, so the # negation binds TIGHTER than the sub-expression and `!falsy.zero' parses as # `(!falsy).zero'. That negates the OBJECT -- true, so false -- and then asks a # boolean for a field, which `visit_field' answers null for. MEASURED: # `!falsy.zero' is `null', where `!(falsy.zero)' is `false'. # # Neither an error nor the answer the reader wanted, which is what makes it # worth a case rather than a comment: an expression written without the # parentheses is silently wrong at every call, and `null' is a perfectly # ordinary JMESPath result that nothing downstream will flag. # `truth-not-dotted-unparenthesised' pins the surprising half and # `truth-not-zero' the intended one; the pair is the only way to state it. # `truth-not-value' and `truth-not-object' take a BARE identifier and answer # correctly, which is what localises the surprise to the dot rather than to `!'. # # Upstream 1.0.1's behaviour, and specified: JMESPath's own grammar gives # `not-expression' a higher precedence than `sub-expression'. Not this # platform's, and not a defect to file. # # ============================================================================ # ONE COMPARISON RAISES AN EXCEPTION THE PACKAGE'S OWN HIERARCHY DOES NOT COVER. # ============================================================================ # # The ordering comparators are number-only in the specification. This # implementation admits STRINGS as well, on purpose -- `visitor.py's # `_is_comparable' says so in a comment: "The spec doesn't officially support # string types yet, but enough people are relying on this behavior that it's # been added back". So `` `"a"` < `"b"` `` answers `true' here and would answer # `null' on a conforming implementation. That is a deliberate upstream # extension, asserted as one. # # MIXING the two is where it goes wrong. `_is_comparable' is true for the string # AND for the number, so the guard that returns null for incomparable operands # does not fire, `operator.lt(str, int)' is reached, and a BARE `TypeError' # escapes -- the one place this package raises something that is not a # `JMESPathError'. A caller writing `except jmespath.exceptions.JMESPathError' # around `search()' does not catch it, and the expression that triggers it # (`machines[?name > `1`]', a filter comparing a string field to a number # literal) is an ordinary typo rather than an exotic input. # # `cmp-mixed-types', `cmp-mixed-in-filter' and `caught-as-jmespath-mixed-cmp' # assert it, and `cmp-mixed-equality' is the foil: equality never orders, so it # answers `false' rather than raising, which localises the defect to the # ordering path. Upstream 1.0.1's, not this platform's. Nothing is filed for it # -- see docs/conventions/no-public-upstream-reports.md -- and if a later # release fixes it these three cases go red and say exactly what changed. # # ============================================================================ # TWO RUNTIME DEPENDENCIES THE RECIPE DOES NOT DECLARE, AND ONE IS FATAL. # ============================================================================ # # `jmespath/lexer.py' line 1 is `import string', and OE splits the stdlib # `string' module out as python3-stringold, which python3-core does not rdepend # on. meta-python's recipe declares only python3-math, python3-json and # python3-numbers -- MEASURED off the built .deb, the control stanza is # `Depends: python3-core, python3-json, python3-math, python3-numbers'. On an # image without python3-stringold `import jmespath' raises ModuleNotFoundError # and the whole package is decoration. # # `bin/jp.py' line 6 is `import pprint', reached by `jp.py --ast', which is # under-declared the same way and fails less severely. # # python3-jmespath_%.bbappend adds both. Both are already in this image's # IMAGE_INSTALL for other packages' sake, so it installs nothing new -- which is # the point of it: today the import works by the accident of a neighbour having # pulled `string' in, and the port is not allowed to rest on the accident. # `pkg-depends-*' asserts the corrected line and `stdlib-*' asserts each of the # nine module-scope imports by name, one at a time, so a future stdlib split # that moved one of them says which. # # ============================================================================ # WHAT IS DELIBERATELY NOT HERE. # ============================================================================ # # THE UPSTREAM TEST SUITE. jmespath.py ships `tests/' in its sdist -- pytest # modules plus the JSON compliance corpus -- and the recipe does not `inherit # ptest', so nothing is packaged and there is no -ptest deb to run. Forcing one # would land python3-pytest and its dependency tree for the sake of one package, # which this distro declines for the reason recorded on 99-python3-webcolors. # `pkg-no-ptest' asserts none is installed, so a future image that grew one is # told to run it rather than go on shipping the reproduction below as if it were # the only option. What the compliance corpus exercises -- the constructs, the # functions and the failure paths -- is reproduced in the probe as an explicit # table, which needs no test framework. # # THE `jp.py' CLI'S BEHAVIOUR. The script is asserted to be INSTALLED and # executable and no more. Its interesting arm is `--ast', which needs pprint, # and running it would be a fourth interpreter start for a wrapper around # `compile()' that the probe already exercises directly. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the bytes unpacked from the # built python3-jmespath_1.0.1-r0_wasm32.deb -- not against the sdist -- so what # was measured is what this image installs. The control interpreter is CPython # 3.12.3 and the image ships 3.12, so there is no minor-version gap to reason # across. The package is a parse and a dict walk over ASCII with no # version-dependent construct in it. # # The on-target result is not yet measured -- the batch verify is this port's # first run on wasm32-hwjs -- and the test plan records the expectation as the # prediction it is. P=python3-jmespath # THE CANARY, which asserts nothing about python3-jmespath. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). 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 PKG=$SITE/jmespath DI=$SITE/jmespath-1.0.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-jmespath.list PROBE=$BST_DATA/python3-jmespath-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-jmespath.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import jmespath' 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 on sys.path is distinguishable # from one that imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A FILE -- eight modules, the opposite shape from # 99-python3-smartypants' `py_modules' single file. check $P/package-dir test -d "$PKG" check $P/no-module-file test ! -f "$SITE/jmespath.py" # ALL EIGHT SOURCE FILES. The API is re-exported through __init__.py, so a build # that lost one of the other seven would still import; each is named because # "the package is there" is not the same statement as "the package is whole". for _m in __init__ ast compat exceptions functions lexer parser visitor; do check $P/source-$_m test -s "$PKG/$_m.py" done # ...AND ALL EIGHT PRECOMPILED. This is what `import-quiet' below rests on: OE # writes these with checked-hash invalidation, so the source is hashed and never # recompiled. A missing one is a module compiled from source on every import. for _m in __init__ ast compat exceptions functions lexer parser visitor; do check $P/pycache-$_m test -s "$PKG/__pycache__/$_m.cpython-312.pyc" done # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. This image's python3 # is one static binary with an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a .so could not be loaded at all; this is the case that # fails a version bump which grew one, at install rather than at `import'. # # 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). # BRINTOS-WORKAROUND: brintos/linux#73 remove 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)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. `/usr/bin/jp.py' is exempted BY NAME rather than # by a `bin' rule, because it is itself a .py and the exemption would otherwise # be unnecessary; the case below is what asserts it is there. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in /usr/bin/jp.py) continue ;; *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ...AND IT INSTALLS EXACTLY ONE EXECUTABLE. Unlike 99-python3-webcolors this # package is not a pure library: setup.py's `scripts=['bin/jp.py']' puts a CLI in # ${bindir}. It is asserted present and executable and no further -- see the # header on why its behaviour is not exercised here. check_eq $P/pkg-bindir-exact "/usr/bin/jp.py" \ "$(grep -E '^/usr/(bin|sbin)/' "$W/pkgf" 2>/dev/null)" check $P/script-present test -f /usr/bin/jp.py check $P/script-executable test -x /usr/bin/jp.py # The script is reached through its SHEBANG, so the interpreter it names has to # be one this image resolves. A `#!/usr/bin/env python3' that outlived the env # binary is a CLI that cannot start, and nothing above would say so. check_out $P/script-shebang "python" head -n 1 /usr/bin/jp.py # ----------------------------------------------------------- the dist-info --- # The installed metadata. This package carries its own `__version__', so the # dist-info is the SECOND of two independent statements of the version rather # than the only one, and `version-agrees' below is what compares them. 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.0.1" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import jmespath' has to match, and a wheel that renamed the package would # change it here first. check_out $P/dist-info-top-level "jmespath" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. The # `py3-none-any' tag says the same thing from the other end. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points. Note the NAME: this package ships # LICENSE.txt, not the plain LICENSE that 99-python3-webcolors asserts. check $P/dist-info-license test -s "$DI/LICENSE.txt" check_out $P/dist-info-license-id "License: MIT" cat "$DI/METADATA" # ------------------------------------------------------------- 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-jmespath 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 the -dev or -dbg # stanza of the same family. # BRINTOS-WORKAROUND: brintos/linux#73 remove sed -n '/^Package: python3-jmespath$/,/^$/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, as dpkg recorded them -- and this is the case the # bbappend exists for. See the header: meta-python declares three sub-packages # and jmespath imports FIVE, so python3-jmespath_%.bbappend adds # python3-stringold (lexer.py's `import string', without which `import jmespath' # raises) and python3-pprint (jp.py's, for `--ast'). # # The exact line is asserted as well as the members, because the failure this # guards is a DROPPED name rather than a wrong one: `pkg-depends-stringold' # going red is the port breaking, and `pkg-depends-exact' is what would catch a # sixth name arriving unnoticed. Every one of the five is a sub-package of the # already-ported python3 (BRINTOS-1476), so this port waits on nothing. 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-math "python3-math" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" check_out $P/pkg-depends-stringold "python3-stringold" cat "$W/dep" check_out $P/pkg-depends-pprint "python3-pprint" cat "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-json, python3-math, python3-numbers, python3-pprint, python3-stringold" \ "$(cat "$W/dep")" # NO -ptest PACKAGE IS INSTALLED. See the header: the recipe does not `inherit # ptest' at all, so this is a statement that nothing else produced one. It tells # a future image that grew one to RUN it rather than go on shipping the # reproduction in the probe as if it were the only option. check_eq $P/pkg-no-ptest "" \ "$(sed -n '/^Package: python3-jmespath-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If jmespath cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable, and -- see the header -- the exact thing a # missing python3-stringold breaks. check $P/import python3 -c 'import jmespath' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode rather than about style. OE writes the .pyc files with # checked-hash invalidation, so the sources are hashed and never recompiled. A # red here means a shipped .pyc was REJECTED -- a stale hash, a python3 minor # bump that changed the magic, or a missing __pycache__ -- and the package is # being compiled from source on every import. check_eq $P/import-quiet "" \ "$(python3 -c 'import jmespath' 2>&1)" # ---------------------------------------------------------------- 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" 600 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 `^' and `=' anchors keep `fn_min' and # `fn_min_by' apart, which is the pair this suite most needs to distinguish. 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 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 # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "171" "$(pv emitted_count)" # ------------------------------------------------------------- the identity --- # WHICH COPY ANSWERED. Every case below is a statement about the package named # here; without this one they are statements about whatever was first on # sys.path. check_out $P/probe-package-path "$PKG/__init__.py" printf '%s' "$(pv package_file)" # It IS a package. A release that collapsed the eight modules into one would # still import and still pass every behavioural case below. check_eq $P/is-package "yes" "$(pv is_package)" # The version, from the module's own `__version__' and from the installed # dist-info independently. Two statements, so a wheel built from the wrong tree # is distinguishable from a mislabelled one. check_eq $P/version-attr "1.0.1" "$(pv version_attr)" check_eq $P/version-dist "1.0.1" "$(pv dist_version)" # THE NINE MODULE-SCOPE STDLIB IMPORTS, one at a time. See the header: OE splits # the stdlib and this recipe declared three of the five splits these need, so # `stdlib-string' is the case the bbappend exists for and would be the first to # red on an image that dropped python3-stringold. The other eight are here so a # future split that moved one of them says WHICH, rather than reporting a bare # import failure. for _m in string random math json numbers operator inspect itertools warnings; do check_eq $P/stdlib-$_m "yes" "$(pv stdlib_$_m)" done # --------------------------------------------- identifiers and sub-expressions # The two constructs everything else is built on: a field, and a field of a # field. check_eq $P/id-field '"hello world"' "$(pv id_field)" check_eq $P/id-subexpr "30" "$(pv id_subexpr)" check_eq $P/id-deep '"found"' "$(pv id_deep)" # A MISSING KEY IS NULL, NOT AN ERROR, AND IT IS NULL AT EVERY LEVEL. This is the # single most load-bearing property of the language for a caller -- it is why an # expression can be applied to a document whose shape is not known -- so the # last-level and mid-path forms are separate cases. check_eq $P/id-missing "null" "$(pv id_missing)" check_eq $P/id-missing-mid "null" "$(pv id_missing_mid)" # ...and a path through a NON-OBJECT is null too, not a type error. `visit_field' # catches AttributeError, and a string has no `.get'. check_eq $P/id-through-scalar "null" "$(pv id_through_scalar)" # The identity expression `@' is the whole document. check_eq $P/id-identity-type '"object"' "$(pv id_identity_type)" # ----------------------------------------------------------------- indexing --- # Index expressions, including the negative form the specification requires. check_eq $P/idx-first "4" "$(pv idx_first)" check_eq $P/idx-last "2" "$(pv idx_last)" # OUT OF RANGE IS NULL, NOT AN ERROR -- the other half of the shape-agnostic # property above, and the case that separates JMESPath's indexing from the host # language's, which raises IndexError. check_eq $P/idx-out-of-range "null" "$(pv idx_out_of_range)" check_eq $P/idx-negative-out-of-range "null" "$(pv idx_negative_out_of_range)" # An index applied to a STRING is null, not a character: JMESPath has no string # indexing even though the implementation language does. check_eq $P/idx-on-string "null" "$(pv idx_on_string)" # ------------------------------------------------------------------- slices --- # `[start:stop:step]', every part optional, half-open like the host language's. # The stepped and negative forms are here because they are the ones an # implementation is most likely to get wrong; the fixture is [4,-1,10,2], chosen # unsorted so a slice cannot be confused with a sort. check_eq $P/slice-range "[4,-1]" "$(pv slice_range)" check_eq $P/slice-open-start "[4,-1]" "$(pv slice_open_start)" check_eq $P/slice-open-stop "[10,2]" "$(pv slice_open_stop)" check_eq $P/slice-step "[4,10]" "$(pv slice_step)" check_eq $P/slice-reverse "[2,10,-1,4]" "$(pv slice_reverse)" check_eq $P/slice-negative "[10,2]" "$(pv slice_negative)" # A slice that selects nothing is the EMPTY ARRAY, not null. That distinction is # what the truthiness group rests on: `[]' is a value, and it is one the # specification calls false. check_eq $P/slice-empty "[]" "$(pv slice_empty)" check_eq $P/slice-on-string "null" "$(pv slice_on_string)" # -------------------------------------------------------------- projections --- # A LIST WILDCARD projects the right-hand side over every element and DROPS the # nulls. `proj-drops-null' is the case that shows the drop: the fixture's fourth # machine has no `state', so four elements project to three. A reader expecting # a null-padded list of four gets the wrong length everywhere downstream, which # is why the length is asserted separately rather than left implicit. check_eq $P/proj-list '["Seattle","New York","Bellevue","Olympia"]' "$(pv proj_list)" check_eq $P/proj-drops-null '["running","stopped","running"]' "$(pv proj_drops_null)" check_eq $P/proj-drops-null-len "3" "$(pv proj_drops_null_len)" # A HASH WILDCARD projects over an object's VALUES. check_eq $P/proj-hash "[30,25]" "$(pv proj_hash)" # A projection of a non-array is null, which is how a wildcard applied to the # wrong shape stays quiet rather than raising. check_eq $P/proj-on-scalar "null" "$(pv proj_on_scalar)" # FLATTEN COLLAPSES ONE LEVEL PER `[]', and the three cases together are what say # so: the fixture is three deep, one `[]' over a two-deep value gives scalars, # and `[][]' over the same value is idempotent because there is nothing left to # collapse. check_eq $P/flat-once "[1,2,3,4]" "$(pv flat_once)" check_eq $P/flat-twice "[[1,2],[3,4],[5],[6,7]]" "$(pv flat_twice)" check_eq $P/flat-of-flat "[1,2,3,4]" "$(pv flat_of_flat)" # ------------------------------------------------------- filter projections --- # `[?expr]' keeps the elements the expression is TRUE for, by the # specification's truthiness rather than the host language's. check_eq $P/filter-eq '["Seattle","Bellevue","Olympia"]' "$(pv filter_eq)" check_eq $P/filter-ne '["New York"]' "$(pv filter_ne)" check_eq $P/filter-gt '["b","c"]' "$(pv filter_gt)" check_eq $P/filter-lte '["a","d"]' "$(pv filter_lte)" # A BOOLEAN expression inside the filter: `&&', `||' and `!' are usable there, # and they are the same operators the truthiness group asserts below. check_eq $P/filter-and '["a","c"]' "$(pv filter_and)" check_eq $P/filter-or '["b","d"]' "$(pv filter_or)" # `filter-not' KEEPS the machine with no `state' -- `!null' is true -- where # `filter-missing-key' drops it. Same document, same missing key, opposite # answers, which is the pair that says a filter on an absent key drops the # element rather than raising. check_eq $P/filter-not '["b","d"]' "$(pv filter_not)" check_eq $P/filter-missing-key '["a","c"]' "$(pv filter_missing_key)" # A filter matching nothing is the empty array, not null. check_eq $P/filter-none "[]" "$(pv filter_none)" # --------------------------------------------------- the ordering comparators # STRINGS ORDER HERE AND DO NOT IN THE SPECIFICATION -- a deliberate upstream # extension, see the header. Both directions, so an implementation that returned # a constant would fail one of them. check_eq $P/cmp-string-lt "true" "$(pv cmp_string_lt)" check_eq $P/cmp-string-gt "false" "$(pv cmp_string_gt)" # ...AND MIXING A STRING WITH A NUMBER RAISES A BARE TypeError, which is the one # exception this package lets out that is not a JMESPathError. See the header: # the filter form is an ordinary typo, not an exotic input. If a later release # fixes this, these cases go red and say exactly what changed. check_eq $P/cmp-mixed-types "" "$(pv cmp_mixed_types)" check_eq $P/cmp-mixed-in-filter "" "$(pv cmp_mixed_in_filter)" # The foil: EQUALITY never orders, so it answers false rather than raising. The # pair is what localises the defect to the ordering path. check_eq $P/cmp-mixed-equality "false" "$(pv cmp_mixed_equality)" # ------------------------------------------------------------- multiselect --- # A multiselect LIST builds an array from several expressions; a multiselect # HASH builds an object with the keys the expression names. Both are how a # JMESPath expression RESHAPES a document rather than merely selecting from it. check_eq $P/multi-list '["Seattle","WA"]' "$(pv multi_list)" check_eq $P/multi-hash '{"City":"Seattle","State":"WA"}' "$(pv multi_hash)" check_eq $P/multi-list-projected \ '[["Seattle","WA"],["New York","NY"],["Bellevue","WA"],["Olympia","WA"]]' \ "$(pv multi_list_projected)" check_eq $P/multi-hash-projected \ '[{"n":"a"},{"n":"b"},{"n":"c"},{"n":"d"}]' "$(pv multi_hash_projected)" # A MULTISELECT KEEPS ITS NULLS WHERE A PROJECTION DROPS THEM. Same document, # same missing key, opposite answer from `proj-drops-null' above -- which is the # pair that says the dropping belongs to the projection and not to the key. check_eq $P/multi-hash-missing '{"n":"d","s":null}' "$(pv multi_hash_missing)" check_eq $P/multi-list-keeps-null '["d",null]' "$(pv multi_list_keeps_null)" # -------------------------------------------------------------------- pipe --- # `|' STOPS a projection and starts a new one over its result, which is the only # way to apply an index or a function to a projection's OUTPUT rather than to # each of its elements. check_eq $P/pipe-basic '"Seattle"' "$(pv pipe_basic)" check_eq $P/pipe-length "4" "$(pv pipe_length)" check_eq $P/pipe-chained '"b"' "$(pv pipe_chained)" # WITHOUT the pipe the same text means something else: `[0]' inside the # projection indexes each NAME -- a string, which JMESPath does not index -- so # every element yields null and the projection drops them all, leaving `[]'. # This is the pair that shows the pipe is load-bearing rather than punctuation, # and it is the shape a reader is most likely to write by mistake. check_eq $P/pipe-absent-differs "[]" "$(pv pipe_absent_differs)" # ------------------------------------------------------ the truthiness rules --- # See the header. The specification calls exactly five values false and `0' is # NOT among them; these six `||' cases are that statement, value by value. check_eq $P/truth-or-empty-list '"fallback"' "$(pv truth_or_empty_list)" check_eq $P/truth-or-empty-obj '"fallback"' "$(pv truth_or_empty_obj)" check_eq $P/truth-or-empty-str '"fallback"' "$(pv truth_or_empty_str)" check_eq $P/truth-or-null '"fallback"' "$(pv truth_or_null)" check_eq $P/truth-or-false '"fallback"' "$(pv truth_or_false)" # THE ONE THAT DIFFERS FROM THE HOST LANGUAGE: zero is TRUE, so `||' returns it # rather than falling through. A port that got this wrong passes all five above. check_eq $P/truth-or-zero "0" "$(pv truth_or_zero)" # `||' RETURNS A VALUE, not a boolean -- the left operand where it is true. check_eq $P/truth-or-returns-value '"hello world"' "$(pv truth_or_returns_value)" # `&&' is the mirror: the LEFT operand where it is false, the right one # otherwise. `truth-and-zero-left' is zero's case again, from the other side. check_eq $P/truth-and-false-left "[]" "$(pv truth_and_false_left)" check_eq $P/truth-and-true-left '"right"' "$(pv truth_and_true_left)" check_eq $P/truth-and-zero-left '"right"' "$(pv truth_and_zero_left)" # `!' NEGATES BY THE SAME RULES -- AND THE PARENTHESES ARE LOAD-BEARING. See the # header: `not' binds at 45 and `dot' at 40, so `!falsy.zero' is `(!falsy).zero' # and answers `null' rather than `false'. The unparenthesised case is pinned # first, then the five values it was meant to be asking about. check_eq $P/truth-not-dotted-unparenthesised "null" "$(pv truth_not_dotted_unparenthesised)" check_eq $P/truth-not-zero "false" "$(pv truth_not_zero)" check_eq $P/truth-not-empty-list "true" "$(pv truth_not_empty_list)" check_eq $P/truth-not-empty-obj "true" "$(pv truth_not_empty_obj)" check_eq $P/truth-not-empty-str "true" "$(pv truth_not_empty_str)" check_eq $P/truth-not-null "true" "$(pv truth_not_null)" check_eq $P/truth-not-false "true" "$(pv truth_not_false)" # A BARE identifier needs no parentheses, which is what localises the surprise # above to the dot rather than to `!'. check_eq $P/truth-not-value "false" "$(pv truth_not_value)" check_eq $P/truth-not-object "false" "$(pv truth_not_object)" # EQUALITY SPECIAL-CASES 0/1 AGAINST true/false, because the host language's # `1 == True' is true and JMESPath's is not. These two are the whole of # `_is_special_number_case's observable effect, and the third is the foil that # says equality still works at all. check_eq $P/truth-one-eq-true "false" "$(pv truth_one_eq_true)" check_eq $P/truth-zero-eq-false "false" "$(pv truth_zero_eq_false)" check_eq $P/truth-true-eq-true "true" "$(pv truth_true_eq_true)" # ---------------------------------------------------- the built-in functions --- # The specification's function library. Each is called over the fixture with a # literal expected value, because the failure a spot check passes is a function # TABLE built from `_func_'-prefixed methods by a metaclass at class-creation # time: if the metaclass did not run, every name below is unknown at once, and if # one decorator were lost, only that one is. check_eq $P/fn-length-str "11" "$(pv fn_length_str)" check_eq $P/fn-length-array "4" "$(pv fn_length_array)" check_eq $P/fn-length-obj "2" "$(pv fn_length_obj)" check_eq $P/fn-keys '["alice","bob"]' "$(pv fn_keys)" check_eq $P/fn-values '[{"age":30},{"age":25}]' "$(pv fn_values)" check_eq $P/fn-sort-numbers "[-1,2,4,10]" "$(pv fn_sort_numbers)" check_eq $P/fn-sort-strings '["alpha","bravo","charlie","delta"]' "$(pv fn_sort_strings)" # The `&expr' expression-reference functions. `sort_by' is asserted through the # resulting NAMES rather than the objects, so a red names the element that moved. check_eq $P/fn-sort-by '["d","a","c","b"]' "$(pv fn_sort_by)" check_eq $P/fn-min-by '"d"' "$(pv fn_min_by)" check_eq $P/fn-max-by '"b"' "$(pv fn_max_by)" # `to_string' of a STRING is the identity, not a re-quoted string; of anything # else it is compact JSON. The pair is what says the function is not `json.dumps' # applied blindly. check_eq $P/fn-to-string-number '"42"' "$(pv fn_to_string_number)" check_eq $P/fn-to-string-object '"{\"age\":25}"' "$(pv fn_to_string_object)" check_eq $P/fn-to-string-string '"hello world"' "$(pv fn_to_string_string)" # `to_number' RETURNS NULL rather than raising for an unconvertible value, and # for a bool, a list or an object -- the only function here whose failure path is # a value rather than an exception, which is why all three are named. check_eq $P/fn-to-number-string "42" "$(pv fn_to_number_string)" check_eq $P/fn-to-number-float "1.5" "$(pv fn_to_number_float)" check_eq $P/fn-to-number-garbage "null" "$(pv fn_to_number_garbage)" check_eq $P/fn-to-number-bool "null" "$(pv fn_to_number_bool)" check_eq $P/fn-to-number-array "null" "$(pv fn_to_number_array)" # `type' over all six JMESPath types. This is the function that would expose a # TYPES_MAP built for the wrong host-language type names, and the `boolean' and # `number' pair is where that would show first -- the host makes `bool' a # subclass of `int' and JMESPath does not. check_eq $P/fn-type-string '"string"' "$(pv fn_type_string)" check_eq $P/fn-type-number '"number"' "$(pv fn_type_number)" check_eq $P/fn-type-array '"array"' "$(pv fn_type_array)" check_eq $P/fn-type-object '"object"' "$(pv fn_type_object)" check_eq $P/fn-type-null '"null"' "$(pv fn_type_null)" check_eq $P/fn-type-boolean '"boolean"' "$(pv fn_type_boolean)" check_eq $P/fn-join '"delta, alpha, charlie, bravo"' "$(pv fn_join)" # `map' KEEPS the nulls a projection drops -- the third statement of that # distinction, and the clearest, because the expression differs only in the verb. check_eq $P/fn-map '["a","b","c","d"]' "$(pv fn_map)" check_eq $P/fn-map-keeps-null '["running","stopped","running",null]' "$(pv fn_map_keeps_null)" check_eq $P/fn-contains-array "true" "$(pv fn_contains_array)" check_eq $P/fn-contains-string "true" "$(pv fn_contains_string)" check_eq $P/fn-contains-absent "false" "$(pv fn_contains_absent)" # `not_null' is NULL-based and not falsiness-based: it returns the first # non-null, which for this fixture is the ZERO the truthiness group is about. # That is the case that would catch an implementation using `if argument:'. check_eq $P/fn-not-null-first "0" "$(pv fn_not_null_first)" check_eq $P/fn-not-null-all-null "null" "$(pv fn_not_null_all_null)" # The remaining specification functions, each once: they are cheap, and a table # with holes in it is a table nobody can read as coverage. check_eq $P/fn-abs "3" "$(pv fn_abs)" check_eq $P/fn-avg "3.75" "$(pv fn_avg)" check_eq $P/fn-ceil "2" "$(pv fn_ceil)" check_eq $P/fn-floor "1" "$(pv fn_floor)" check_eq $P/fn-max "10" "$(pv fn_max)" check_eq $P/fn-min "-1" "$(pv fn_min)" check_eq $P/fn-sum "15" "$(pv fn_sum)" check_eq $P/fn-merge '{"a":1,"b":2}' "$(pv fn_merge)" check_eq $P/fn-reverse-array "[2,10,-1,4]" "$(pv fn_reverse_array)" check_eq $P/fn-reverse-string '"dlrow olleh"' "$(pv fn_reverse_string)" check_eq $P/fn-starts-with "true" "$(pv fn_starts_with)" check_eq $P/fn-ends-with "true" "$(pv fn_ends_with)" check_eq $P/fn-to-array-scalar "[1]" "$(pv fn_to_array_scalar)" check_eq $P/fn-to-array-array "[4,-1,10,2]" "$(pv fn_to_array_array)" # --------------------------------------------------------------- compile() --- # `compile()' returns a REUSABLE parsed expression whose `search()' must agree # with the one-shot call. It is the whole of the package's performance story -- a # long-running caller parses once -- and `compile-reused' is what would catch a # parsed result that captured the first document it was applied to. check_eq $P/compile-type "ParsedResult" "$(pv compile_type)" check_eq $P/compile-search '["Seattle","New York","Bellevue","Olympia"]' "$(pv compile_search)" check_eq $P/compile-matches-oneshot "yes" "$(pv compile_matches_oneshot)" check_eq $P/compile-reused '["Reused"]' "$(pv compile_reused)" # The parsed AST is reachable and is the plain dict `ast.py' builds, which is # what `jp.py --ast' prints. The ROOT node carries no `value', which is why the # key list is two long and not three. check_eq $P/compile-ast-keys '["children","type"]' "$(pv compile_ast_type)" # ----------------------------------------------------------- the parser cache --- # The Parser caches the 128 most recent expressions ON THE CLASS, so a second # parse of the same text is the same object and the cache is process-global # state. Both facts matter to a caller measuring parse cost, and `purge()' is the # only handle on the second. check_eq $P/cache-hit-is-same-object "yes" "$(pv cache_hit_is_same_object)" check_eq $P/cache-max-size "128" "$(pv cache_max_size)" check_eq $P/cache-purged-empty "yes" "$(pv cache_purged_empty)" # ------------------------------------------------------- literals and JSON --- # A `` `...` `` literal is parsed as JSON, so every JSON type is expressible in # an expression rather than only in the document. check_eq $P/lit-number "1.5" "$(pv lit_number)" check_eq $P/lit-string '"literal"' "$(pv lit_string)" check_eq $P/lit-true "true" "$(pv lit_true)" check_eq $P/lit-null-type '"null"' "$(pv lit_null_type)" check_eq $P/lit-array "[1,2]" "$(pv lit_array)" check_eq $P/lit-object '{"k":"v"}' "$(pv lit_object)" # A RAW STRING literal `'...'` is NOT JSON and needs no escaping -- the construct # 1.0 added for exactly the quoting pain the case above shows. check_eq $P/lit-raw-string '"raw string"' "$(pv lit_raw_string)" # A quoted IDENTIFIER is a different construct from a string literal: it selects # a key whose name is not a bare identifier. The two are one character apart in # the source text and a lexer that conflated them would pass every case above. check_eq $P/quoted-identifier '"found"' "$(pv quoted_identifier)" # NON-ASCII SURVIVES THE ROUND TRIP. Asserted as CODE POINTS, never as the # characters, so the case does not depend on the encoding the guest's stdout # happens to be in -- `cafe' with an acute e is 99,97,102,233. check_eq $P/unicode-codepoints "99,97,102,233" "$(pv unicode_codepoints)" # ------------------------------------------------------- the failure paths --- # WHICH EXCEPTION, BY NAME. Half this package's contract is its failure path -- # these are the branches a caller writes -- so the type is asserted and the # message deliberately is not: upstream's wording is not part of the # specification and a release that reworded it would red every case here while # the contract was intact. # # The first three are all ParseError or a subclass and are still three different # names, which is the distinction a caller reporting a syntax error to a human # needs: a stray character, a well-formed prefix that stops, and a token that is # legal elsewhere. check_eq $P/raise-lexer "LexerError" "$(pv raise_lexer)" check_eq $P/raise-incomplete "IncompleteExpressionError" "$(pv raise_incomplete)" check_eq $P/raise-parse "ParseError" "$(pv raise_parse)" check_eq $P/raise-empty "EmptyExpressionError" "$(pv raise_empty)" check_eq $P/raise-unterminated-quote "LexerError" "$(pv raise_unterminated_quote)" check_eq $P/raise-unknown-function "UnknownFunctionError" "$(pv raise_unknown_function)" # ARITY is checked before types and is its own exception -- and the variadic form # has its own class again, which is the pair a caller cannot get from the message. check_eq $P/raise-arity-few "ArityError" "$(pv raise_arity_few)" check_eq $P/raise-arity-many "ArityError" "$(pv raise_arity_many)" check_eq $P/raise-variadic-arity "VariadictArityError" "$(pv raise_variadic_arity)" # THE WRONG TYPE RAISES RATHER THAN RETURNING NULL. This is the case the ticket # names, and the one a caller is most likely to have assumed the other way round: # `length' of a number is an error, not zero and not null. `sort_by' is included # because its check is the LATE one -- the type of what the expression reference # resolves to, not of what was passed -- and it still lands on the same class. check_eq $P/raise-type-length "JMESPathTypeError" "$(pv raise_type_length)" check_eq $P/raise-type-join "JMESPathTypeError" "$(pv raise_type_join)" check_eq $P/raise-type-sort "JMESPathTypeError" "$(pv raise_type_sort)" check_eq $P/raise-type-sort-by "JMESPathTypeError" "$(pv raise_type_sort_by)" # ...AND THEY ARE ALL CATCHABLE AS `JMESPathError' -- except the mixed-type # comparison, which is the header's finding stated as the one case that answers # `no'. Asserting the two greens beside the one exception is what makes the hole # visible as a hole rather than as an omission. check_eq $P/caught-as-jmespath-lexer "yes" "$(pv caught_as_jmespath_lexer)" check_eq $P/caught-as-jmespath-type "yes" "$(pv caught_as_jmespath_type)" check_eq $P/caught-as-jmespath-mixed-cmp "no:TypeError" "$(pv caught_as_jmespath_mixed_cmp)" # The hierarchy's root is a ValueError, which is what a caller predating the # package's own exceptions is catching and what a release must not change. check_eq $P/exc-base-is-valueerror "yes" "$(pv exc_base_is_valueerror)" # ------------------------------------------------------------------ Options --- # `Options(dict_cls=)' picks the class a MULTISELECT HASH builds, which is this # package's one documented extension point for output shape, and the ORDER is the # expression's rather than the document's -- which is the reason to ask for an # OrderedDict at all. The expression names `b' before `a' deliberately, so a # result that came back document-ordered is distinguishable from one that came # back expression-ordered. check_eq $P/options-dict-cls "OrderedDict" "$(pv options_dict_cls)" check_eq $P/options-dict-order '["b","a"]' "$(pv options_dict_order)" rm -rf "$W"