. "$BST_LIB" # BST-TIMEOUT: 1600 # FOUR python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # third start left behind. # # # # The four are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), the probe, which takes # # ONE start for a hundred and sixty-one measurements, and # # the console script, which is a fourth python3 by way of # # /usr/bin/objectpath. # # # # The work inside the probe is parsing sixty short # # expressions and walking a document of four books. # # Microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line of the # # program runs. # # # # 1600 is 400s per start, the same per-start budget # # 99-python3-imagesize and 99-python3-iso8601 set for # # theirs, and roughly six times the ~60s a python3 start has # # measured at here. Nothing in this suite is quadratic in # # anything, so the start is the only thing the number has to # # cover. # # # # A suite-timeout red is a batch-wide cost, so if the four # # starts prove slower than that 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-objectpath 0.6.1 (meta-openembedded meta-python # recipes-devtools/python/python3-objectpath_0.6.1.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # `Tree(data).execute(expr)' runs a query over an already-parsed Python object # -- nested dicts and lists -- and returns the part of it the query selects. # The language is XPath-shaped: `$' is the root, `.' walks a key, `..' descends # recursively, `[...]' is an index or a predicate, `@' is the node a predicate # is being tested against. On top of that it has arithmetic, comparison, and # about thirty built-in functions -- aggregates, string operations, casts, and a # date family. # # It is nine files of pure Python, 1996 lines, and it reads no file, opens no # socket and touches no device. Every fixture in this suite is a literal in the # probe, so the suite reaches no peer, no terminal and nothing outside its own # process. # # ============================================================================ # THE PORT IS FOUR RDEPENDS, A SUITE AND A PROBE. # ============================================================================ # # No patch, no site answer, no configure step. All ten Python files unpacked # from the built .deb are BYTE-IDENTICAL to the sdist the SRC_URI sha256 pins, # which is the measurement behind "no patch" rather than a reading of the # recipe. The recipe-scope build ran 1634 tasks, all succeeded, zero ERROR: # lines. # # What the recipe does NOT declare is the whole of the bbappend: setuptools3 # gives it `python3-core' and nothing else, and `import objectpath' reaches four # stdlib trees outside that split -- xml (via xml.sax.saxutils), urllib.request # (which the stdlib's own saxutils imports, and which poky's python3-xml does # not rdepend on), datetime, and json. `pkg-depends-exact' below asserts the # line the bbappend produces, so a rootfs that dropped any of them says so here # once rather than presenting as a broken library. # # ============================================================================ # WHAT THIS SUITE FOUND. Each is asserted at the value the library ACTUALLY # produces, so a release that repairs one turns the case that names it red. # ============================================================================ # # root-alone `$' ON ITS OWN RAISES StopIteration. The documented way to # ask for the whole document does not work at all -- the # parser produces an empty program and the interpreter's # `next()' over it comes back empty. `$.*' is the form that # answers, and both are asserted side by side. # # slice-* SLICING IS NOT IMPLEMENTED. `$.store.books[1:3]' is a PARSE # ERROR, and so is every other form -- `[:2]', `[2:]', # `[0:8:2]'. An index works and a slice does not. This # ticket's own acceptance text asked for a selector into an # array "by index and by slice"; the index half exists and the # slice half does not, and the suite asserts the SyntaxError # rather than the feature. # # caret-* `^' IS AN INFIX OPERATOR THAT SILENTLY DISCARDS ITS RIGHT # SIDE. parser.py declares `infix("^", 0)' -- binding power # ZERO, so it never binds -- and the interpreter has no `^' # arm at all. `2 ^ 10' is therefore neither an error nor 1024: # it is 2. `1 + 2 ^ 3' is 3. The right-hand side of the # expression is not evaluated and not reported. # # matches-* `matches' HAS ITS OPERANDS THE WRONG WAY ROUND. The arm is # `re.match(exe(node[1]), exe(node[2]))', so the LEFT operand # is compiled as the PATTERN and the right is the subject. # `'abc' matches 'a.c'' is false and `'a.c' matches 'abc'' is # true, which is the swap stated as a pair rather than as a # failure to match. `re' is worse: its arm returns # `re.compile(left)' and never looks at the right operand, so # the expression's value is a compiled pattern object. # # reverse-mutates `reverse()' REWRITES THE DOCUMENT IT WAS GIVEN. The arm # calls the in-place list method and returns the same object, # so a read-only query mutates the caller's data and every # later query sees the new order. `sort-leaves-doc' is the # control: sort() on the same document does not, which is what # makes this a defect rather than a documented convention. # # map-upper `map()' FLATTENS ITS RESULTS INTO CHARACTERS. The arm is # `chain(*map(...))', and chaining strings iterates them, so # mapping `upper' over four words returns 22 one-character # strings instead of four words. The function is unusable for # any string-valued mapping. # # date-* THE DATE FAMILY IS THE ONE PART OF THE LANGUAGE THAT DOES # NOT WORK AT ALL. `date('2015-01-02')' IGNORES ITS ARGUMENT # and returns TODAY; `dateTime()' returns None for a # well-formed ISO timestamp; and because it does, `toMillis' # and `age' raise AttributeError and TypeError on None rather # than answering. `time()' raises on a string it was handed. # Four documented functions, none of them usable. # # type-* `type()' ANSWERS IN TWO DIFFERENT VOCABULARIES. A dict is # "object" and a list is "array" -- JSON names -- while a # string is "str", an integer is "int" and null is # "NoneType", which are Python names. The module even defines # PY_TYPES_MAP = {"int": "number", "str": "string", ...} for # exactly this, and the `type' arm never reads it. Both the # map's contents and the answers are asserted, so the case # shows the translation that exists and is not used. # # expr-cache-* THE EXPRESSION CACHE IS A MODULE-LEVEL GLOBAL keyed on the # expression TEXT alone and shared by every Tree in the # process. That is a correctness statement, not a performance # one. # # register-leaks `register_function' WRITES TO THE CLASS. `_REGISTERED_ # FUNCTIONS' is a class attribute and the method mutates it in # place, so a function registered on one Tree becomes callable # from every OTHER Tree in the process -- including ones built # before the call. Asserted in all three directions. # # cli-* THE CONSOLE SCRIPT IS DEAD ON ARRIVAL ON THIS IMAGE, and the # cause is a NameError in the handler for a missing optional # dependency. objectpath/shell.py does # # try: # import pytz # except ImportError: # if os.isatty(sys.stdin.fileno()) and ... # # and `os' is never imported in that file -- its imports are # argparse, sys and readline. pytz is not on this image and is # not an RDEPENDS of this recipe, so the except branch runs on # every invocation and raises before argparse is reached. The # package's LIBRARY half is unaffected; only the command is. # # syntaxwarning-* EVERY MODULE COMPILES WITH SyntaxWarning ON 3.12 -- 22 of # them across three files, all `x is '. They are # invisible at import here because the .pyc ships # pre-compiled, which is why the count is taken from # `compile()' in the probe rather than from the import's # stderr. One of them is not cosmetic: timeutils.py line 39 is # `years is 1 and "rok" or ...', so which branch it takes # depends on CPython's small-int cache. # # ============================================================================ # WHAT WORKS, AND IS ASSERTED SO THAT IT KEEPS WORKING. # ============================================================================ # # Named selectors, nested selectors, indexing including negative indices, `..' # recursive descent, `*' on an object, predicates over numbers, strings, `in', # `not in', `and', `or' and `is not', the four arithmetic operators with # precedence and parentheses, string concatenation, the five comparison # operators, the aggregates (sum, avg, min, max, count, len), fifteen string and # cast functions, keys/values, sort with and without a key, and the literals # true/false/null. Sixty expressions in all, each with its result asserted # literally. # # WHICH RESULTS ARE LAZY is asserted separately from what they contain # (`class-*'), because a query returning the right values in the wrong shape is # the failure a value comparison alone would not catch: a selector over an array # is a generator, `..' is an itertools.chain, and an index or a plain key is the # object itself. `gen-second-pass' asserts the consequence -- these are # one-shot, so a caller who stores a result and iterates it twice gets the # values and then nothing. # # AN EMPTY RESULT IS THREE DIFFERENT THINGS depending on the query, and all # three are asserted rather than assumed: a missing key is None, a predicate # that matches nothing is an empty generator, and a `..' descent for a key that # is not there is an empty chain. None of them raises, which is what the # ticket's acceptance text asked to have pinned. P=python3-objectpath bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/objectpath DI=$SITE/objectpath-0.6.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-objectpath.list PROBE=$BST_DATA/python3-objectpath-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-objectpath.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A PACKAGE OF THREE SUBPACKAGES, not a flat module: objectpath/ holds the # language in core/ and everything else in utils/. A release that flattened it # would still import and still pass every behavioural case below, so the shape # is asserted on its own. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/no-flat-module test ! -f "$SITE/objectpath.py" check $P/init-file test -s "$PKG/__init__.py" check $P/core-dir test -d "$PKG/core" check $P/utils-dir test -d "$PKG/utils" check $P/interpreter-file test -s "$PKG/core/interpreter.py" check $P/parser-file test -s "$PKG/core/parser.py" check $P/shell-file test -s "$PKG/shell.py" check $P/timeutils-file test -s "$PKG/utils/timeutils.py" check $P/json-ext-file test -s "$PKG/utils/json_ext.py" # THE UPSTREAM TEST SUITE IS NOT PACKAGED. setup.py names its three packages # explicitly rather than calling find_packages(), so unlike python3-aniso8601 # there is no `tests' subpackage riding along in the runtime .deb. check $P/no-tests-dir test ! -d "$PKG/tests" check $P/no-py-typed test ! -e "$PKG/py.typed" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-interp test -s "$PKG/core/__pycache__/interpreter.cpython-312.pyc" check $P/pycache-parser test -s "$PKG/core/__pycache__/parser.cpython-312.pyc" check $P/pycache-shell test -s "$PKG/__pycache__/shell.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". This image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying a compiled extension could not be imported here at all. # objectpath ships no C -- no .c, no .pyx, no ext_modules in setup.py -- so this # is a REGRESSION TRIPWIRE on a version bump rather than a statement about # today. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python and the one console script in the payload, which is # the same claim from the other side: a data file appearing here would be a # packaging change nobody asked for. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; /usr/bin/objectpath) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.6.1" cat "$DI/METADATA" check_out $P/dist-info-name "Name: objectpath" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `setuptools3' BUILT THIS, WHICH IS VISIBLE IN THE WHEEL METADATA and is the # half of the recipe's `inherit' line that leaves a trace on the rootfs. The # `py2-none-any' tag is upstream's `universal' wheel setting -- a package last # released in 2017 still declaring a Python 2 tag -- and `zip-safe' is an # EGG-ERA MARKER that setuptools carried through into the dist-info directory. # Both are worth pinning so that a repackaging shows up here rather than # silently. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "objectpath" cat "$DI/top_level.txt" check $P/dist-info-zip-safe test -e "$DI/zip-safe" check_out $P/dist-info-entry-point \ "objectpath = objectpath.shell:main" cat "$DI/entry_points.txt" sed -n '/^Package: python3-objectpath$/,/^$/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" check_out $P/pkg-version "Version: 0.6.1-r0" cat "$W/st" # THE FOUR DECLARED SPLITS ARE THE WHOLE OF THE PORT, and each is load-bearing # at MODULE scope -- there is no arm of this package that avoids any of them. # Measured on an x86-64 control by refusing one tree at a time with a meta_path # hook: xml gives ImportError, urllib.request gives ImportError, datetime gives # ImportError, and json gives a bare `Exception: JSONNotFound' because # json_ext.py re-raises rather than propagating. python3-netclient is the one # that looks wrong and is not: objectpath imports no URL machinery, but CPython's # own xml/sax/saxutils.py opens with `import os, urllib.parse, urllib.request' # and poky's python3-xml rdepends on `core' alone. The line is asserted exactly, # so a lean image that dropped any of them would say so here once. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-datetime, python3-json, python3-netclient, python3-xml" \ "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import objectpath' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. It is also where the 22 compile-time # SyntaxWarnings would surface if the shipped .pyc ever stopped being used, so # this case and `syntaxwarning-total' below are two halves of one statement. check_eq $P/import-quiet "" \ "$(python3 -c 'import objectpath' 2>&1)" # ----------------------------------------------------------------- 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 bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } 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 probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "161" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here. check_eq $P/package-answered "$PKG/__init__.py" "$(pv package_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/interpreter-name "objectpath.core.interpreter" "$(pv interpreter_module)" check_eq $P/has-tree "yes" "$(pv has_Tree)" check_eq $P/has-debugger "yes" "$(pv has_Debugger)" check_eq $P/dist-version "0.6.1" "$(pv dist_version)" # THE PACKAGE EXPORTS BY WILDCARD AND DECLARES NOTHING. `objectpath/__init__.py' # is two `from ... import *' lines with no `__all__' anywhere in the tree, so # the public surface is whatever those modules happen to define -- 35 names, # including `re', `sys', `chain' and `islice' re-exported by accident. There is # no `__version__' either, so a caller has to go to the installed metadata for # it, which is what `dist-version' above does. check_eq $P/no-dunder-all "no" "$(pv has_dunder_all)" check_eq $P/no-version-attr "no" "$(pv has_version_attr)" # THE FOUR SPLITS, FROM THE INTERPRETER'S OWN ANSWER rather than from the # Depends line: these are the modules `import objectpath' actually reached. # Three of the four are reached unconditionally. The fourth, `json', is NOT -- # json_ext.py's first arm is `import simplejson as json' and this image ships # simplejson, so the stdlib fallback below it is never taken and stdlib `json' # is absent from sys.modules. python3-json stays an RDEPENDS all the same, # because it is what answers on an image without simplejson, which is not one: # see `json-impl' below. The two cases are the same fact read from both ends. check_eq $P/closure-xml "yes" "$(pv closure_xml_sax_saxutils)" check_eq $P/closure-urllib "yes" "$(pv closure_urllib_request)" check_eq $P/closure-datetime "yes" "$(pv closure_datetime)" check_eq $P/closure-json "no" "$(pv closure_json)" # SIMPLEJSON IS THE ONLY NON-STDLIB MODULE REACHED. The delta is taken across # the import from the module table as it stood before it, so this is the # import's own closure and not a census of the interpreter. simplejson is in it # for the reason `closure-json' gives; nothing else outside the standard library # is, and the day a second name appears here the port has grown a dependency # nobody declared. pytz is the one optional dependency objectpath probes for and # it is NOT on this image -- which is the premise of the `cli-*' cases below, # and the reason it is asserted here rather than assumed there. check_eq $P/closure-nonstd "simplejson" "$(pv nonstd_closure)" check_eq $P/pytz-absent "no" "$(pv pytz_importable)" # simplejson WINS THE json IMPORT ON THIS IMAGE. json_ext.py tries simplejson # first and falls back to the stdlib; this image ships python3-simplejson for # other reasons, so the fallback is not taken and objectpath's `str()' formats # through simplejson. It is deliberately NOT an RDEPENDS -- the package works # without it -- so the day the image drops it, this case is how that is noticed. check_eq $P/json-impl "simplejson" "$(pv json_impl)" check_eq $P/json-fast-impl "simplejson" "$(pv json_fast_impl)" # ------------------------------------------------------- source warnings ----- # See the header. Taken from `compile()', not from the import's stderr, so this # is a statement about the SOURCE and stays true whatever the .pyc does. check_eq $P/syntaxwarning-total "22" "$(pv syntaxwarning_total)" check_eq $P/syntaxwarning-files \ "interpreter.py:4|json_ext.py:2|timeutils.py:16" "$(pv syntaxwarning_files)" # ------------------------------------------------------------- selectors ----- check_eq $P/sel-name "Emcraft" "$(pv sel_name)" check_eq $P/sel-bool "true" "$(pv sel_bool)" check_eq $P/sel-nested "deep" "$(pv sel_nested)" check_eq $P/sel-idx0 "Sayings of the Century" "$(pv sel_idx0)" check_eq $P/sel-idx-neg "The Lord of the Rings" "$(pv sel_idx_neg)" check_eq $P/sel-object "{color:red,price:19.95}" "$(pv sel_bicycle)" # A KEY APPLIED TO AN ARRAY IS APPLIED TO EVERY ELEMENT -- there is no `[*]' to # write, and this is the workhorse of the language. check_eq $P/sel-all-titles \ "Sayings of the Century|Sword of Honour|Moby Dick|The Lord of the Rings" \ "$(pv sel_all_titles)" # ------------------------------------------- recursive descent / wildcard ---- # `..' FINDS A KEY AT ANY DEPTH AND IN DOCUMENT ORDER. `desc-price' reaches the # four books and the bicycle; `desc-color' reaches one key three levels down. check_eq $P/desc-price "8.95|12.99|8.99|22.99|19.95" "$(pv desc_price)" check_eq $P/desc-title \ "Sayings of the Century|Sword of Honour|Moby Dick|The Lord of the Rings" \ "$(pv desc_title)" check_eq $P/desc-color "red" "$(pv desc_color)" # `*' ON AN OBJECT HANDS BACK THE OBJECT ITSELF, not its values -- so `$.store.*' # and `$.store' are the same thing. Asserted as class plus key list, because # what the case is about is the shape. check_eq $P/wild-store-class "dict" "$(pv wild_store_class)" check_eq $P/wild-store-keys "bicycle|books|name|open" "$(pv wild_store_keys)" # `$..*' WALKS EVERY OBJECT NODE AND ONLY OBJECT NODES. Ten of them here: the # root, `store', the four books, `bicycle', and the three levels of `nested'. # Arrays and scalars are not yielded, which is why the count is a case of its # own beside the all-dicts assertion. check_eq $P/wild-any-count "10" "$(pv wild_any_count)" check_eq $P/wild-any-all-dicts "yes" "$(pv wild_any_all_dicts)" # ------------------------------------------------------------ predicates ----- check_eq $P/pred-num "Sword of Honour|The Lord of the Rings" "$(pv pred_num)" check_eq $P/pred-str "Sayings of the Century" "$(pv pred_str)" check_eq $P/pred-both "Sayings of the Century|Moby Dick" "$(pv pred_both)" check_eq $P/pred-or "Sword of Honour|The Lord of the Rings" "$(pv pred_or)" check_eq $P/pred-in "Moby Dick|The Lord of the Rings" "$(pv pred_in)" check_eq $P/pred-not-in "Sayings of the Century|Sword of Honour" "$(pv pred_not_in)" check_eq $P/pred-isnot \ "Sayings of the Century|Moby Dick|The Lord of the Rings" "$(pv pred_isnot)" # ------------------------------------------------------------ arithmetic ----- check_eq $P/ar-add "3" "$(pv ar_add)" check_eq $P/ar-sub "2" "$(pv ar_sub)" check_eq $P/ar-mul "12" "$(pv ar_mul)" check_eq $P/ar-div "3.5" "$(pv ar_div)" check_eq $P/ar-mod "1" "$(pv ar_mod)" check_eq $P/ar-paren "9" "$(pv ar_paren)" check_eq $P/ar-field "39.9" "$(pv ar_field)" check_eq $P/ar-concat "ab" "$(pv ar_concat)" # `+' COERCES TO float BEFORE IT CONCATENATES, so a number plus a string is a # ValueError out of float() rather than a type error the language raised. The # message names the string, which is why only the class is asserted. check_eq $P/ar-num-plus-str "!ValueError" "$(pv ar_num_plus_str)" check_eq $P/cmp-lt "true" "$(pv cmp_lt)" check_eq $P/cmp-ge "true" "$(pv cmp_ge)" check_eq $P/cmp-is "true" "$(pv cmp_is)" check_eq $P/cmp-isnot "true" "$(pv cmp_isnot)" check_eq $P/cmp-in-str "true" "$(pv cmp_in_str)" # ------------------------------------------------------------ aggregates ----- check_eq $P/agg-sum-price "53.92" "$(pv agg_sum_price)" check_eq $P/agg-sum-counts "31" "$(pv agg_sum_counts)" check_eq $P/agg-avg "3.875" "$(pv agg_avg)" check_eq $P/agg-min "1" "$(pv agg_min)" check_eq $P/agg-max "9" "$(pv agg_max)" check_eq $P/agg-count "4" "$(pv agg_count_books)" check_eq $P/agg-len-str "4" "$(pv agg_len_str)" check_eq $P/agg-count-str "3" "$(pv agg_count_str)" # THE AGGREGATES DISAGREE ABOUT THE EMPTY CASE. sum() and count() answer 0; # avg() divides by len() and raises ZeroDivisionError; min() propagates # Python's own ValueError. Three behaviours for one shape of input, all # asserted, because a caller aggregating a filtered result hits it routinely. check_eq $P/agg-sum-empty "0" "$(pv agg_sum_empty)" check_eq $P/agg-count-empty "0" "$(pv agg_count_empty)" check_eq $P/agg-avg-empty "!ZeroDivisionError" "$(pv agg_avg_empty)" check_eq $P/agg-min-empty "!ValueError" "$(pv agg_min_empty)" # sum() FILTERS ITS INPUT TO NUMBERS AND SAYS NOTHING. Four strings sum to 0 -- # not an error, not a concatenation, just zero. So `sum($.words)' and # `sum($.empty)' are indistinguishable to a caller. check_eq $P/agg-sum-strings "0" "$(pv agg_sum_strings)" # --------------------------------------------------------------- strings ----- check_eq $P/fn-upper "ABC" "$(pv fn_upper)" check_eq $P/fn-lower "abc" "$(pv fn_lower)" check_eq $P/fn-capitalize "Hello world" "$(pv fn_capitalize)" check_eq $P/fn-title "Hello World" "$(pv fn_title)" check_eq $P/fn-split "a|b|c" "$(pv fn_split)" check_eq $P/fn-join "delta-alpha-charlie-bravo" "$(pv fn_join)" check_eq $P/fn-replace "bonono" "$(pv fn_replace)" check_eq $P/fn-slice "bc" "$(pv fn_slice)" check_eq $P/fn-str "12" "$(pv fn_str)" check_eq $P/fn-int "12" "$(pv fn_int)" check_eq $P/fn-float "1.5" "$(pv fn_float)" check_eq $P/fn-round "1.57" "$(pv fn_round)" # join() WITH NO SEPARATOR JOINS ON THE EMPTY STRING. The arm catches the # IndexError and defaults to "", so a caller who forgets the second argument # gets one run-on word rather than a diagnostic. check_eq $P/fn-join-default "deltaalphacharliebravo" "$(pv fn_join_default)" # escape()/unescape() ARE xml.sax.saxutils' -- the reason python3-xml is a # dependency at all. escape() covers `&', `<' and `>' and NOT the quotes; # applying it twice escapes the ampersand it introduced, which is the ordinary # XML-escaping trap and is asserted so the behaviour is on the record. check_eq $P/fn-escape "<a>&" "$(pv fn_escape)" check_eq $P/fn-unescape "" "$(pv fn_unescape)" check_eq $P/fn-escape-twice "&lt;" "$(pv fn_escape_twice)" check_eq $P/fn-sort "1|1|2|3|4|5|6|9" "$(pv fn_sort)" check_eq $P/fn-sort-words "alpha|bravo|charlie|delta" "$(pv fn_sort_words)" check_eq $P/fn-sort-key \ "Sayings of the Century|Moby Dick|Sword of Honour|The Lord of the Rings" \ "$(pv fn_sort_key)" check_eq $P/fn-keys "color|price" "$(pv fn_keys)" check_eq $P/fn-values "red|19.95" "$(pv fn_values)" check_eq $P/fn-list \ "Sayings of the Century|Sword of Honour|Moby Dick|The Lord of the Rings" \ "$(pv fn_list)" # keys() ON AN ARRAY IS AN ExecutionError, which is one of the two exception # classes the package defines. Worth pinning because the OTHER one -- # ProgrammingError -- is what an unknown function raises, and the split between # them is the only diagnostic structure the language has. check_eq $P/fn-keys-on-array "!ExecutionError" "$(pv fn_keys_on_array)" # ------------------------------------------------------------------ type ----- # Two vocabularies in one function -- see the header. PY_TYPES_MAP is asserted # beside the answers because it is the translation the module defines and the # `type' arm never reads. check_eq $P/type-obj "object" "$(pv type_obj)" check_eq $P/type-arr "array" "$(pv type_arr)" check_eq $P/type-str "str" "$(pv type_str)" check_eq $P/type-int "int" "$(pv type_int)" check_eq $P/type-float "float" "$(pv type_float)" check_eq $P/type-bool "bool" "$(pv type_bool)" check_eq $P/type-null "NoneType" "$(pv type_null)" check_eq $P/py-types-map-str "string" "$(pv py_types_map_str)" check_eq $P/py-types-map-int "number" "$(pv py_types_map_int)" check_eq $P/lit-true "true" "$(pv lit_true)" check_eq $P/lit-false "false" "$(pv lit_false)" check_eq $P/lit-null "null" "$(pv lit_null)" # ------------------------------------------------------------ generators ----- # WHICH RESULTS ARE LAZY, as the class name rather than inferred from the # values. See the header: a query returning the right values in the wrong shape # is what a value comparison alone would not catch. check_eq $P/class-all-titles "generator" "$(pv class_all_titles)" check_eq $P/class-desc "chain" "$(pv class_desc)" check_eq $P/class-pred "generator" "$(pv class_pred)" check_eq $P/class-idx "dict" "$(pv class_idx)" check_eq $P/class-name "str" "$(pv class_name)" check_eq $P/class-array "list" "$(pv class_array)" check_eq $P/gen-first-pass \ "Sayings of the Century|Sword of Honour|Moby Dick|The Lord of the Rings" \ "$(pv gen_first_pass)" check_eq $P/gen-second-pass "" "$(pv gen_second_pass)" # ---------------------------------------------------- the three empties ------ # A missing key is None, a predicate that matched nothing is an EMPTY GENERATOR, # and a `..' descent for an absent key is an EMPTY CHAIN. None of the three # raises, and a caller cannot tell the first from a key whose value is null. check_eq $P/empty-missing-key "null" "$(pv sel_missing_key)" check_eq $P/empty-missing-deep "null" "$(pv sel_missing_deep)" check_eq $P/empty-class-missing "NoneType" "$(pv class_missing)" check_eq $P/empty-pred "" "$(pv pred_empty)" check_eq $P/empty-pred-class "generator" "$(pv class_pred)" check_eq $P/empty-desc "" "$(pv desc_missing)" check_eq $P/empty-desc-class "chain" "$(pv class_desc_missing)" # ---------------------------------------------------------------- errors ----- # A SYNTACTICALLY INVALID QUERY RAISES, and which class it raises is not one # thing: an unclosed bracket comes out of the tokenizer as TokenError, an # unknown function is the package's own ProgrammingError, and a type mismatch in # an operator is ProgrammingError too. Only the classes are asserted -- the # messages carry ANSI escapes and upstream's wording. check_eq $P/err-unclosed "!TokenError" "$(pv err_unclosed)" check_eq $P/err-unknown-fn "!ProgrammingError" "$(pv err_unknown_fn)" check_eq $P/err-bad-op "!ProgrammingError" "$(pv err_bad_op)" check_eq $P/err-keys-no-arg "!SyntaxError" "$(pv err_keys_no_arg)" check_eq $P/err-reverse-str "!AttributeError" "$(pv err_reverse_str)" # THE ERROR MESSAGE CARRIES ANSI ESCAPES UNCONDITIONALLY. utils/colorify.py has # no tty check anywhere in it, so an application logging the exception writes # SGR codes into its log file. Both halves are asserted: that the raw message # contains ESC[, and what it says once they are stripped. check_eq $P/err-msg-has-ansi "yes" "$(pv err_msg_has_ansi)" check_eq $P/err-msg-stripped "Function nosuchfn does not exist." \ "$(pv err_msg_stripped)" # ============================================================================ # THE FINDINGS. Asserted at the values the library ACTUALLY produces -- see the # header for what each one is. A release that repairs one turns the case that # names it red, which is the intent. # ============================================================================ # `$' alone does not work; `$.*' is the form that answers. check_eq $P/root-alone "!StopIteration" "$(pv root_alone)" check_eq $P/root-star-class "dict" "$(pv root_star_class)" check_eq $P/root-star-keys "counts|empty|nested|s|store|words" \ "$(pv root_star_keys)" # `@' OUTSIDE A PREDICATE IS LEFTOVER INTERPRETER STATE: None on a Tree that has # answered nothing, and the last node walked on one that has. The same query, # two answers, decided by what ran before it. check_eq $P/current-fresh "null" "$(pv current_fresh)" check_eq $P/current-after-query-class "dict" "$(pv current_after_query_class)" # SLICING IS NOT IMPLEMENTED -- four forms, four parse errors. check_eq $P/slice-range "!SyntaxError" "$(pv slice_range)" check_eq $P/slice-open-left "!SyntaxError" "$(pv slice_open_left)" check_eq $P/slice-open-right "!SyntaxError" "$(pv slice_open_right)" check_eq $P/slice-step "!SyntaxError" "$(pv slice_step)" # `^' DISCARDS ITS RIGHT SIDE. check_eq $P/caret-pow "2" "$(pv caret_pow)" check_eq $P/caret-in-sum "3" "$(pv caret_in_sum)" check_eq $P/caret-str "a" "$(pv caret_str)" # `matches' TAKES ITS OPERANDS BACKWARDS, and `re' ignores its right one. check_eq $P/matches-subject-pattern "false" "$(pv matches_subject_pattern)" check_eq $P/matches-pattern-subject "true" "$(pv matches_pattern_subject)" check_eq $P/re-op-returns-pattern "str" "$(pv re_op_class)" # `reverse()' MUTATES; `sort()' is the control that does not. check_eq $P/reverse-result "6|2|9|5|1|4|1|3" "$(pv reverse_result)" check_eq $P/reverse-mutates "6|2|9|5|1|4|1|3" "$(pv reverse_mutates)" check_eq $P/sort-result "1|1|2|3|4|5|6|9" "$(pv sort_result)" check_eq $P/sort-leaves-doc "3|1|4|1|5|9|2|6" "$(pv sort_leaves_doc)" # `map()' FLATTENS STRINGS INTO CHARACTERS. check_eq $P/map-upper "D|E|L|T|A|A|L|P|H|A|C|H|A|R|L|I|E|B|R|A|V|O" \ "$(pv map_upper)" check_eq $P/map-class "chain" "$(pv map_class)" # THE DATE FAMILY. date() ignores its argument, dateTime() answers None, and the # two functions built on dateTime() raise on that None. check_eq $P/date-class "date" "$(pv date_class)" check_eq $P/date-ignores-arg "yes" "$(pv date_ignores_arg)" check_eq $P/datetime-iso-null "null" "$(pv datetime_iso)" check_eq $P/now-class "datetime" "$(pv now_class)" check_eq $P/err-time "!AttributeError" "$(pv err_time)" check_eq $P/err-to-millis "!AttributeError" "$(pv err_to_millis)" check_eq $P/err-age "!TypeError" "$(pv err_age)" # THE EXPRESSION CACHE IS A PROCESS-WIDE GLOBAL keyed on the expression text. check_eq $P/expr-cache-global "yes" "$(pv expr_cache_is_global)" check_eq $P/expr-cache-grows "yes" "$(pv expr_cache_grows)" check_eq $P/expr-cache-reused "yes" "$(pv expr_cache_reused)" # `register_function' WRITES TO THE CLASS, so it reaches every other Tree. check_eq $P/unregistered-fn "!ProgrammingError" "$(pv unregistered_fn)" check_eq $P/registered-fn "21" "$(pv registered_fn)" check_eq $P/register-leaks-other "21" "$(pv register_leaks_to_other_tree)" check_eq $P/register-leaks-new "21" "$(pv register_leaks_to_new_tree)" # ------------------------------------------------------------------- cli ----- # The console script is packaged, is a correct setuptools entry-point wrapper, # and cannot run on this image. See the header: the handler for a missing pytz # calls `os.isatty' in a file that never imports `os'. check $P/cli-present test -x /usr/bin/objectpath check_eq $P/cli-shebang "#!/usr/bin/env python3" "$(head -n 1 /usr/bin/objectpath)" check_out $P/cli-wrapper-imports-main \ "from objectpath.shell import main" cat /usr/bin/objectpath # THE CAUSE, from the probe's own import of the module. check_eq $P/cli-readline-ok "yes" "$(pv readline_importable)" check_eq $P/cli-shell-import "NameError" "$(pv shell_import)" check_eq $P/cli-shell-no-os "no" "$(pv shell_source_imports_os)" # ...and the consequence, from the real binary. Every invocation fails the same # way, including `--help', which is what makes it dead on arrival rather than # degraded: argparse is never reached. printf '{"a": 1}\n' > "$W/doc.json" check_rc $P/cli-help-fails 1 objectpath --help check_rc $P/cli-query-fails 1 objectpath -e '$.a' "$W/doc.json" check_out $P/cli-traceback "Traceback (most recent call last)" \ sh -c 'objectpath --help 2>&1 || true' rm -rf "$W"