. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE 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 strips comments from a 343-byte document and # # parses six small ones, which is nothing beside the # # interpreter load. The three starts are `import', # # `json-import' and the probe -- this package ships no # # script, so there is no fourth. # # # # 1200 is 99-python3-jsonpointer's and 99-python3-elementpath's # # figure, for the same reason: the start dominates and the # # work after it is comparable. # # # # 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-jstyleson 0.0.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-jstyleson_0.0.2.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image -- JSON WITH JAVASCRIPT-STYLE # COMMENTS. `jstyleson.loads(text)' parses the dialect config files are actually # written in: `//' and `/* */' comments, and a trailing comma before a closing # bracket. `json.loads' rejects all three. # # ============================================================================ # WHAT THE PORT ADDS, AND WHY IT IS NOT `re.sub(r"//.*", "", text)'. # ============================================================================ # # A reader will reasonably ask why a package is needed to delete comments, since # a two-line regex appears to do it. The answer is QUOTING. A JSON document's # string values may contain `//' and `/* */' -- URLs, glob patterns, prose -- and # a stripper that does not track whether it is inside a string literal eats them # out of the DATA. That failure is silent in the worst way: the document still # parses, and the corruption is in a value. # # So the interesting behaviour is entirely in the STATE MACHINE -- in-string, # in-line-comment, in-block-comment, escaped -- and that is what the cases below # are about. The fixture carries a `note' value holding both `//' and `/* */', # and `note-value' asserts it comes back whole. Asserting only that a commented # document parses would be asserting the part the regex does too. # # ============================================================================ # THE CONTROL IS THE STDLIB, NOT A TRANSCRIBED LITERAL. # ============================================================================ # # `loads-eq-hand' compares jstyleson.loads(FIXTURE) against json.loads of the # SAME document written out by hand with the comments and trailing commas # removed. That is the package's definition, stated as an equality between two # independent readings rather than as an expected value somebody typed. # # `stdlib-refuses' is the other half and it is what fails a NO-OP INSTALL: the # stdlib must REJECT the fixture. Together the two say the package did the work. # Either on its own could pass on an image where `jstyleson' had degraded to a # plain `json' alias. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, ONE MODULE, ONE stdlib IMPORT. # ============================================================================ # # 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. # # jstyleson ships NO C: no .c, .pyx or .so in the sdist, and setup.py declares # `py_modules' with no ext_modules at all. Its only import is `json' itself, # which is python3-json, a sub-package of the ported python3 (BRINTOS-1476), and # that is the recipe's whole RDEPENDS -- so nothing about this port waits on # another package. `pkg-no-so' below is a REGRESSION TRIPWIRE on a version bump # rather than a statement about today. # # IT IS A SINGLE MODULE, NOT A PACKAGE. setup.py declares # `py_modules=['jstyleson']', so what lands in site-packages is the FILE # jstyleson.py -- the same shape as 99-python3-jsonpointer and the opposite of # 99-python3-elementpath's directory of subpackages. `no-package-dir' asserts it, # because a release that grew a package directory would still import and still # pass every behavioural case while the shape this image ships had changed. # # ============================================================================ # THE VERSION IS STATED ONCE, NOT TWICE, AND THAT IS A FACT ABOUT THE MODULE. # ============================================================================ # # This module declares NO `__version__'. 99-python3-jsonpointer can assert its # version from the module AND from importlib.metadata and cross-check the two; # here there is only the installed dist-info, so `dist-version' and the # dist-info directory name are the two independent statements instead. # `no-version-attr' asserts the absence directly rather than leaving it as an # unexplained gap between the two suites -- and a release that GREW one would # announce itself there. # # ============================================================================ # WHAT IS NOT COVERED. # ============================================================================ # # The recipe does NOT `inherit ptest' and upstream's sdist ships no tests # directory at all (setup.py names `test_suite="tests"', which is not in the # tarball), so there is no upstream suite to run here or anywhere. These cases # are the only statement about this package on this image. # # `dumps'/`dump' are `json.dumps'/`json.dump' verbatim -- jstyleson WRITES no # comments, it only reads them -- so they are asserted as AGREEMENT with the # stdlib rather than against literals. That is the whole of their promise. # # ============================================================================ # ONE UPSTREAM LIMITATION IS ASSERTED AS ONE. # ============================================================================ # # dispose removes a `//' comment when it meets the NEWLINE that ends it, so a # `//' comment at end of input with NO trailing newline survives and the document # then fails to parse. `eof-comment-kept' and `eof-comment-raises' record that, # with `eof-with-newline' as the control. It is 0.0.2's behaviour on x86-64 and # it is UPSTREAM'S, not this platform's; it is asserted so the port ships a # statement about what it does rather than a silence, and so a release that fixed # it announces itself here instead of somewhere a caller would find it. # # ============================================================================ # WHERE THE ASSERTED VALUES COME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the sdist the recipe's # SRC_URI sha256 pins (680003f3...30e, verified by digest before measuring), and # again against the bytes of the built python3-jstyleson_0.0.2-r0_wasm32.deb. The # control interpreter is CPython 3.10 and the image ships 3.12; that gap is # stated here and in the test plan rather than papered over. The on-target result # is not yet measured -- the batch verify is this port's first run on wasm32-hwjs. P=python3-jstyleson # THE CANARY, which asserts nothing about python3-jstyleson. # # 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 MOD=$SITE/jstyleson.py DI=$SITE/jstyleson-0.0.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-jstyleson.list PROBE=$BST_DATA/python3-jstyleson-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-jstyleson.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import jstyleson' 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" # ...AND IT IS A FILE, NOT A DIRECTORY. See the header: a release that grew a # package directory would still import and would still pass every behavioural # case below, while the shape this image ships had silently changed. check_eq $P/no-package-dir "absent" \ "$(if [ -d "$SITE/jstyleson" ]; 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)" # 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 README -- without having to enumerate what it # might be. This package ships no script at all, so unlike # 99-python3-jsonpointer's equivalent there is no executable to exempt: anything # outside site-packages is unexpected here by construction. # # 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 on THIS package is the only machine-readable # statement of the version -- the module declares no `__version__' (see the # header and `no-version-attr' below). The directory name carries it a second # time in $DI itself, and `$P/dist-version' reads it through importlib.metadata, # which is the reading a caller would get. 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.0.2" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import jstyleson' has to match, and a wheel that renamed the module would # change it here first. check_out $P/dist-info-top-level "jstyleson" cat "$DI/top_level.txt" # -------------------------------------------------------------- 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-jstyleson 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-jstyleson$/,/^$/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 DEPENDENCY THE RECIPE DECLARES, as dpkg recorded it. The recipe's # RDEPENDS is `python3-json' alone, a sub-package of the ported python3 # (BRINTOS-1476), which is why this port waits on nothing. python3-core comes # with the packaging. 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" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If jstyleson 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. check $P/import python3 -c 'import jstyleson' # ...and `json' separately, because jstyleson is a WRAPPER around it -- `loads' # is literally `json.loads(dispose(text))' -- so a missing python3-json presents # AS a broken jstyleson. This case is what tells the two apart. check $P/json-import python3 -c 'import json' # ---------------------------------------------------------------- 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 `loads' distinguishable # from `loads_eq_hand', and `dispose_sha' from `fixture_sha'. 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, read through importlib.metadata over the installed # dist-info -- the reading a caller would get, as opposed to the directory name # `dist-info-dir' asserts above. check_eq $P/dist-version "0.0.2" "$(pv dist_version)" # ...and the module carries NO `__version__' of its own. See the header: this is # why there is one statement of the version here and two in # 99-python3-jsonpointer, and a release that grew one should say so here rather # than leave the difference between the two suites unexplained. check_eq $P/no-version-attr "no" "$(pv has_version_attr)" # The module answered out of the file this image installed, not from a stray copy # on sys.path. This is what makes the payload cases above and the behaviour cases # below statements about the SAME jstyleson. check_eq $P/module-answered "$MOD" "$(pv module_file)" # THE PUBLIC NAMES, one case each. This module declares no `__all__', so dir() # would report what is BOUND rather than what is exported -- including its own # `import json', which is implementation detail. See the probe's header. for _n in dispose loads load dumps dump; do check_eq $P/api-$_n "yes" "$(pv has_$_n)" done # ------------------------------------------------------------- the fixture --- # The JSONC document the probe drove, pinned by length and digest, so every case # below is known to be about that document rather than about an edited probe. The # digest is load-bearing: a stray editor keystroke inside a comment would change # what is being stripped, and this case is what names the common cause of the # reds that would follow. check_eq $P/fixture-len "343" "$(pv fixture_len)" check_eq $P/fixture-sha "5594a87ed3ae45e9" "$(pv fixture_sha)" # ...and the hand-stripped control, pinned the same way. `loads-eq-hand' below is # an equality against THIS text, so a drifted control would turn a correct # library red and this case is what would say so. check_eq $P/hand-sha "1d95ae67207b439e" "$(pv hand_sha)" # --------------------------------------------- THE STDLIB MUST REFUSE IT ----- # What fails a NO-OP INSTALL. Without this, every case below would still pass on # an image where `jstyleson' had become a plain `json' alias AND the fixture were # ordinary JSON; the pair is what says the package did the work. check_eq $P/stdlib-refuses "yes" "$(pv stdlib_refuses)" check_eq $P/stdlib-exc "JSONDecodeError" "$(pv stdlib_exc_type)" # WHERE it stops, which is what says the refusal is about the DIALECT rather than # about some later damage: line 2, column 3, is the `//' opening the first # comment. A stdlib that choked somewhere else would mean the fixture is # malformed in a way that has nothing to do with comments. check_eq $P/stdlib-exc-msg "Expecting property name enclosed in double quotes" \ "$(pv stdlib_exc_msg)" check_eq $P/stdlib-exc-line "2" "$(pv stdlib_exc_line)" check_eq $P/stdlib-exc-col "3" "$(pv stdlib_exc_col)" # ------------------------------------------------- dispose(), THE STRIPPER --- # `loads' is `json.loads(dispose(text))' and nothing else, so every parse case # below is downstream of this one function. It is asserted four ways because each # catches something the others do not. # # THE EXACT BYTES, by digest. dispose DELETES the comment characters rather than # replacing them with spaces, and deletes the newline that ends a `//' comment # along with it -- so the output's whitespace is a real property of the # implementation, and a release that changed the strategy would parse to the same # object while producing different text. check_eq $P/dispose-sha "41ef9fc00d0a3fcb" "$(pv dispose_sha)" # THE SAME TEXT, WHITESPACE COLLAPSED, as the readable form. The digest above is # exact and says nothing a human can check; this one is legible. Note what is # still in it: the `//' and `/* */' inside the `note' VALUE. check_eq $P/dispose-norm \ '{ "host": "example.net", "port": 8080, "note": "a // b and /* c */ stay", "retries": [ 1, 2, 5 ] }' \ "$(pv dispose_norm)" # THE MARKERS THAT SURVIVED, COUNTED. The fixture holds three `//' and one `/*' # outside strings and exactly one of each inside the `note' string, so a correct # dispose leaves one of each. ZERO means the stripper reached inside the string # literal; MORE than one means it missed a comment. A boolean would not separate # those two failures. check_eq $P/dispose-slashslash "1" "$(pv dispose_slashslash)" check_eq $P/dispose-blockopen "1" "$(pv dispose_blockopen)" # THE OUTPUT IS PLAIN JSON, by the STDLIB's reading. `loads' below proves this # through jstyleson's own wrapper; this is the contract a caller relies on when # it disposes and hands the text somewhere else. check_eq $P/dispose-parses "yes" "$(pv dispose_parses_stdlib)" # IDEMPOTENT, stated on the OUTPUT rather than on a comment-free document -- # which is the interesting direction, because the leftover `//' and `/* */' # inside the `note' string are exactly what a second pass would eat if the # quoting were tracked wrongly. check_eq $P/dispose-idempotent "yes" "$(pv dispose_idempotent)" # ------------------------------------------------------ loads(), THE DELIVERABLE # The parse itself, as canonical JSON (sorted keys, no incidental whitespace) so # this is an assertion about CONTENT rather than about dict insertion order. check_eq $P/loads \ '{"host":"example.net","note":"a // b and /* c */ stay","port":8080,"retries":[1,2,5]}' \ "$(pv loads)" # THE EQUALITY THAT DEFINES THE PACKAGE: jstyleson.loads(FIXTURE) equals # json.loads(HAND_STRIPPED), computed in the probe. Not a transcribed literal -- # the right-hand side is the stdlib parsing the same document written without the # dialect. See the header. check_eq $P/loads-eq-hand "yes" "$(pv loads_eq_hand)" # THE STRING LITERAL CAME BACK WHOLE, which is the one case that separates this # package from a regex. A stripper blind to quoting returns `a and stay' here # and passes every other case above. See the header. check_eq $P/note-value '"a // b and /* c */ stay"' "$(pv note_value)" # The trailing comma in the ARRAY left no hole. A stripper that replaced the # comma with `null' rather than deleting it would give [1,2,5,null] and pass a # length-blind check. check_eq $P/retries '[1,2,5]' "$(pv retries)" # --------------------------------------------------------------- the edges --- # The fixture is the realistic case; these are one-line documents that isolate # one rule each, so a red names the rule rather than the document. # # A document with NO dialect in it comes through dispose UNCHANGED. Without this, # a dispose that mangled ordinary JSON would be invisible -- every case above # drives a document dispose is supposed to change. check_eq $P/plain-identity "yes" "$(pv plain_identity)" # TRAILING COMMAS NEST: an array inside an array inside an object, all three with # one, which is where a stripper that only looked at the outermost pair fails. check_eq $P/nested-commas '{"a":[1,[2,3]],"b":{"c":1}}' "$(pv nested_commas)" # AN ESCAPED QUOTE does not end the string, so the `//' after it is still inside # a literal. The quoting rule against the one input that breaks a naive # quote-counter. check_eq $P/escaped-quote '{"s":"he said \"hi\" // not a comment"}' \ "$(pv escaped_quote)" # A BLOCK COMMENT THAT IS THE WHOLE OF A STRING VALUE. `note-value' asserts the # markers survive beside real text; this is the degenerate case, which a stripper # running before the quoting pass would reduce to the empty string. check_eq $P/block-in-string '{"s":"/* not stripped */"}' "$(pv block_in_string)" # ------------------------------------------- the upstream limitation, as one -- # dispose ends a `//' comment at the NEWLINE, so one at end of input with no # trailing newline survives and the document then fails to parse. See the header: # this is 0.0.2's behaviour and it is UPSTREAM'S, not this platform's. It is # asserted so the port ships a statement rather than a silence. check_eq $P/eof-comment-kept "yes" "$(pv eof_comment_kept)" check_eq $P/eof-comment-raises "yes" "$(pv eof_comment_raises)" # ...and the CONTROL: the same document WITH the newline parses, which is what # says the two cases above are about the missing terminator and not about the # comment itself. check_eq $P/eof-with-newline '{"a":1}' "$(pv eof_with_newline)" # ------------------------------------------------ dumps/dump, the write half -- # These are `json.dumps'/`json.dump' verbatim -- jstyleson writes no comments, it # only reads them -- so AGREEMENT with the stdlib is the whole of their promise # and asserting a literal would be asserting the stdlib's formatting. check_eq $P/dumps-eq-json "yes" "$(pv dumps_eq_json)" # ...and with kwargs, which the wrapper forwards. A `**kwargs' that had been # dropped would pass the case above and silently ignore every option. check_eq $P/dumps-kwargs "yes" "$(pv dumps_kwargs_eq_json)" # ------------------------------------------------ load/dump, over a real file -- # `load' is `loads(fp.read())' and `dump' is `json.dump'. The probe drives both # over a real file under TMPDIR rather than a StringIO, because a config parser's # actual use is a path on disk and this image's filesystem is the part that is # new. TMPDIR, never the virtiofs root (brintos/linux#73). check_eq $P/load-file \ '{"host":"example.net","note":"a // b and /* c */ stay","port":8080,"retries":[1,2,5]}' \ "$(pv load_file)" check_eq $P/dump-file "yes" "$(pv dump_file_eq_json)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv probe_complete)" rm -rf "$W"