. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps one probe transcript. # # # # The three: a bare `import webencodings', a second import # # whose STDERR is the assertion, and the probe -- which # # takes ONE start for 124 measurements, upstream's own # # eight test functions among them. This package ships NO # # executable, so there is no shipped program to run as a # # real process and no exit status to assert: nothing here # # needs a fourth start. # # # # The work inside a start is a few thousand dict lookups # # and `codecs` calls over byte strings under 40 bytes, plus # # upstream's suite, which measures 0.004s on the x86-64 # # control. 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, against the ~60s a python3 start # # has measured at here. The start dominates and nothing in # # this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-webencodings 0.5.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-webencodings_0.5.1.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHY IT IS NOT `codecs` WITH EXTRA STEPS. # ============================================================================ # # webencodings is the WHATWG *Encoding* standard's label table: it maps the 214 # encoding names and aliases a browser must accept onto the 41 canonical # encodings they really denote, and wraps Python's own codecs so that decoding # follows the standard's rules rather than the codec module's. # # The difference is not cosmetic, and one case states the whole of it: # # lookup('iso-8859-1').name == 'windows-1252' # # A document labelled `iso-8859-1` is to be decoded as windows-1252, because # that is what browsers have always done and what the bytes 0x80-0x9F in real # web content mean. `b'\x93hi\x94'.decode('iso-8859-1')` gives the C1 control # characters U+0093 and U+0094; this package gives the curly quotes that were # meant. Both readings are asserted below, side by side, because the pair is # what makes the point -- either alone is just a value. # # The standard has no ASCII encoding at all, so `ascii' and `us-ascii' land on # windows-1252 too. That is the same rule seen from the other side. # # ============================================================================ # THE TABLE IS ASSERTED AS A DIGEST, AND THAT IS DELIBERATE. # ============================================================================ # # `labels.py' is a GENERATED file -- `mklabels.py' writes it from the spec's own # encodings.json -- and the failure this port is exposed to is a TRUNCATION: a # packaging step that copied it short would leave `lookup('utf-8')' working # while a few hundred legacy aliases silently answered None. Twenty # hand-written label cases would all pass through that. # # So the whole table is reduced to a SHA-256 over its sorted `labelname' # pairs and the digest is the assertion, with `labels-count' and `names-count' # beside it to say WHICH WAY it moved and a hand-written sample below to say # what a digest mismatch is about. A release that legitimately adds a label # reds here and says so in one line, which is the right cost for a file nobody # reads. # # ============================================================================ # THE FAILURE PATH IS TWO SHAPES, AND WHICH ONE YOU GET DEPENDS ON THE CALL. # ============================================================================ # # lookup(label) returns None for an unknown label. It is the spec's # `get an encoding', which is defined to report failure # as a VALUE -- so `try: lookup(x) except LookupError' # catches nothing and reads None as an encoding. # decode/encode(label) raise LookupError, because `_get_encoding' turns that # None into one. # # Both are asserted. A caller who wrote the wrong one of the two gets no fault # where they expected one, or an unhandled fault where they expected None. # # ============================================================================ # THE BOM OVERRIDES THE DECLARED ENCODING, WHICH IS THE OTHER HALF OF THE PORT. # ============================================================================ # # `decode(input, fallback)' sniffs a UTF-8, UTF-16-LE or UTF-16-BE BOM first and # uses `fallback' only when there is none -- the rule the standard states and # the one a plain `bytes.decode' gets wrong. All three BOMs are asserted against # a declared label that is deliberately something else, so a green case cannot # be the fallback having been used by accident, and the BOM is asserted CONSUMED # rather than decoded into a stray U+FEFF. # # The default error handler is `replace', not `strict' -- the opposite of # `bytes.decode'. Undecodable content in a web document is meant to survive as # U+FFFD rather than abort the parse, and a caller who assumed the stdlib # default gets no exception where they expected one. `encode' defaults the other # way, to `strict'. Both defaults are measured rather than described. # # ============================================================================ # THE INCREMENTAL PATH IS DRIVEN AT EVERY OFFSET, NOT AT ONE. # ============================================================================ # # `iter_decode'/`iter_encode' and the two Incremental classes exist for input # that arrives in chunks, so the interesting boundary is one INSIDE a multi-byte # sequence. The fixture is `a\xe9z' -- a 1-, 2-, 3- and 4-byte # utf-8 sequence in one string -- split at every one of the 12 offsets, with the # result asserted equal to the one-shot call each time. The suite asserts the # COUNT of agreements against the count of splits, and the probe names the first # offset that disagreed, so a failure is one red carrying the offset rather than # a transcript nobody reads. # # The BOM is sniffed ACROSS chunks too, which is the case the one-shot decoder # cannot have: the decoder holds input back until it has three bytes or is told # the input has ended. `iter-decode-short' is the other side of that -- a # one-byte input that never reaches three still resolves, because `final=True' # forces the decision. Without it those bytes would be dropped in silence. # # ============================================================================ # `x-user-defined' IS THE ONE ENCODING PYTHON DOES NOT HAVE. # ============================================================================ # # It maps bytes 0x80-0xFF onto the private-use area U+F780-U+F7FF, which is how # a browser holds bytes it was told not to interpret. `codecs.lookup' does not # know it -- asserted, as `xud-not-in-codecs' -- and this package ships its own # charmap implementation for it in `x_user_defined.py'. The 256-byte round trip # is asserted, so a truncated charmap table is caught rather than the three # bytes a sample would have covered. # # ============================================================================ # UPSTREAM'S OWN EIGHT TESTS RUN ON THIS IMAGE, WITHOUT PYTEST. # ============================================================================ # # setup.py's `find_packages()' takes the whole directory, so `tests.py' lands in # site-packages beside the library. Its eight `test_*' functions are PLAIN # FUNCTIONS with bare `assert' statements -- no pytest, no unittest -- so # nothing outside the package is needed to run them, and the probe simply calls # each one. That is why this works on an image that has neither, and it is a # cheaper arrangement than python3-isodate's, which needs python3-unittest and # gets it only because the image installs it for other reasons. # # `test_all_labels' walks every one of the 214 labels and looks each one up, so # it is the broadest single thing that runs here. The COUNT is asserted, not # just the failure total: a collection that found nothing would report zero # failures and read as green. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND 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), so a module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. webencodings ships five .py files and no C: the # wheel says `Root-Is-Purelib: true' and `pkg-no-so' below says the same thing # about what actually landed. # # The library imports exactly one thing outside itself: `codecs'. The recipe's # RDEPENDS name python3-codecs and python3-json, and the SECOND of those is an # artifact of the dependency scanner rather than a requirement of the library -- # `mklabels.py', a developer tool that regenerates labels.py from the spec over # HTTP, is the only file in the package that says `import json', and using this # library never imports it. `mklabels-not-imported' is the case that says so. # # ============================================================================ # THE CODEC SWEEP, WHICH IS THE ONE CASE ABOUT THE PLATFORM. # ============================================================================ # # webencodings resolves lazily: `lookup' calls `codecs.lookup(python_name)' the # first time a name is asked for, so a codec this image does not carry is a # LookupError at first use and NOT at import. A caller that only ever asks for # utf-8 would never find out. `codecs-missing' sweeps all 41 canonical names in # one pass and names the ones that failed. # # EXPECTED `none', AND THE EXPECTATION IS GROUNDED IN THE BUILT ARTIFACTS # RATHER THAN IN THE SOURCE -- but it is still not a measurement, because # nothing in this port has run on target yet. What was checked: python3-core # ships all 122 `encodings/*.py', the 40 Python-side encodings these names # resolve to among them; and the nine CJK names need C extensions # (`_multibytecodec' and `_codecs_{cn,hk,jp,kr,tw,iso2022}'), every one of which # is present in the 59 MB wasm python3 binary's symbol table, linked in # statically because configure picks MODULE_BUILDTYPE=static from # host_cpu=wasm32. A red here would be a statement about this image's # interpreter and NOT about webencodings. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured by running data/python3-webencodings-probe.py # against the 0.5.1 sdist, verified against the recipe's own SRC_URI sha256 # (b36a1c24...1923) before it was read. Measured TWICE -- under python3.14 and # under python3.12, the version this image ships -- and the two transcripts are # byte-identical apart from the module's own path, so no expectation below # depends on an interpreter version. Neither run is a target run; the target run # is the batch verify. P=python3-webencodings # THE CANARY, which asserts nothing about python3-webencodings. # # 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 PKGDIR=$SITE/webencodings DI=$SITE/webencodings-0.5.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-webencodings.list PROBE=$BST_DATA/python3-webencodings-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-webencodings.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import webencodings' further down is the # functional statement; these say WHICH copy answered, so a package importing # out of some other path on sys.path is distinguishable from one importing out # of what this image installed. check $P/site-packages-dir test -d "$SITE" # A PACKAGE, NOT A MODULE. A release that flattened it to one file would change # the import surface without any behavioural case noticing. check $P/package-dir test -d "$PKGDIR" check $P/package-init test -s "$PKGDIR/__init__.py" check $P/no-flat-module test ! -f "$SITE/webencodings.py" # THE FOUR SUBMODULES BESIDE `__init__', each named. `labels' is imported at # import time and is the payload; `x_user_defined' is imported lazily, only when # that encoding is first asked for; `tests' is upstream's suite, which the probe # runs; `mklabels' is the developer tool nothing imports. for _m in labels mklabels tests x_user_defined; do check $P/module-$(echo "$_m" | tr '_' '-') test -s "$PKGDIR/$_m.py" done # The precompiled bytecode, which `import-quiet' below turns on. check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-labels test -s "$PKGDIR/__pycache__/labels.cpython-312.pyc" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. On this image a .so # could not be loaded at all, so this is the case that fails a version bump # which grew one -- at install rather than at `import'. # # BRINTOS-WORKAROUND: brintos/linux#73 remove -- read `$PKGLIST' directly when # the redirect stops failing. 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. 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)" # ...AND NOTHING IN ${bindir}. webencodings declares no `scripts' and no # `console_scripts': it is a library with no CLI, so there is no executable and # no shebang to assert. Stated as a fact about the package rather than left to # be inferred from a case nobody wrote. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The two cases # above name the failures that are expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so each # entry is 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 --- 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.5.1" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs; a wheel that # renamed the package would change it here first. check_out $P/dist-info-top-level "webencodings" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT -- the packaging-side statement # of `pkg-no-so' above: no platform tag, no extension module, nothing that could # need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # NO LICENCE FILE IN THE DIST-INFO, and that is measured rather than assumed. # This recipe's LIC_FILES_CHKSUM points at line 8 of the sdist's PKG-INFO, which # the wheel does not carry -- so there is nothing here to check the licence text # against, and a case asserting one would red forever. check $P/dist-info-no-license test ! -e "$DI/LICENSE" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files; this is the one that says # python3-webencodings is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. sed -n '/^Package: python3-webencodings$/,/^$/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 THREE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. # Each is asserted by name, because "the Depends line is non-empty" would pass # with any of them missing. 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-codecs "python3-codecs" cat "$W/dep" # ...AND python3-json, WHICH THE LIBRARY DOES NOT USE. See the header: the only # `import json' in this package is in `mklabels.py', a developer tool that # regenerates labels.py from the spec over HTTP, and the scanner cannot tell a # shipped developer tool from a runtime import. Asserted so the over-declaration # is a stated fact rather than something a later reader has to work out -- # `mklabels-not-imported' below is the other half of the pair. check_out $P/pkg-depends-json "python3-json" cat "$W/dep" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If webencodings 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 webencodings' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. A statement about the shipped # bytecode rather than about style: a module compiled from SOURCE at import time # would emit whatever diagnostics 3.12 raises over this code, and the package # ships a precompiled .pyc precisely so the compiler never runs. A red here # means the shipped .pyc was REJECTED -- a stale hash, a python3 minor bump that # changed the magic, or a missing __pycache__ -- and every import on this image # is paying to recompile five modules. check_eq $P/import-quiet "" \ "$(python3 -c 'import webencodings' 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 `lookup_utf8' # distinguishable from `lookup_utf8_noh'. 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, and `emitted-count' is # what catches a key skipped in the middle. It counts every line the probe # wrote, `probe_done' included, so it is the transcript's own `wc -l'. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "126" "$(pv emitted_count)" # The module answered out of the file this image installed, not from a stray # copy on sys.path -- which is what makes the payload cases above and the # behavioural cases below statements about the SAME webencodings. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # ------------------------------------------------------------- the identity --- check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION HAS TWO READINGS AND THEY MUST AGREE. Unlike python3-isodate, # which defines none, this package defines `VERSION' -- and setup.py greps that # same literal out of __init__.py to build the sdist, so the module attribute # and the installed dist-info are two independent renderings of one line. A # disagreement means the wheel was built from a different tree than the one # installed, which no single reading could show. check_eq $P/version "0.5.1" "$(pv version)" check_eq $P/dist-version "0.5.1" "$(pv dist_version)" # It is `VERSION', not `__version__' -- the name PEP 396 would have used and the # one a caller reaches for first. Asserted absent so the reading above is known # to be the only one. check_eq $P/has-dunder-version "no" "$(pv has_dunder_version)" # NO `__all__'. The published surface is whatever __init__.py defines, so there # is no export list to assert and the fourteen names below are the statement # instead. Stated rather than left to be inferred from a case nobody wrote. check_eq $P/has-all "no" "$(pv has_all)" # THE MODULES THAT LOADED, AND THE ONE THAT DID NOT. # `labels' is imported by __init__.py at import time -- it IS the payload, so a # red here is an import failure, not a lazy-load failure. check_eq $P/labels-module "yes" "$(pv labels_module)" # `x_user_defined' is imported LAZILY, only when that encoding is first asked # for. At this point in the probe nothing has asked, so `no' is the correct # answer and a `yes' would mean the lazy import stopped being lazy -- which on # this image costs every importer the 4 KB charmap it will probably never use. check_eq $P/x-user-defined-lazy "no" "$(pv x_user_defined_imported)" # `mklabels' IS THE ONLY MODULE HERE THAT OPENS A SOCKET. It is a developer tool # that regenerates labels.py from the WHATWG spec over HTTP; it ships because # find_packages() takes the whole directory, and its `import json' is the whole # reason the recipe RDEPENDS on python3-json. This is the case that says using # the library does not import it -- the other half of `pkg-depends-json' above. check_eq $P/mklabels-not-imported "yes" "$(pv mklabels_not_imported)" # The fourteen names a caller reaches for, each pinned present. A red here says # the module surface moved, which no behavioural case below could distinguish # from the function itself breaking. check_eq $P/has-lookup "yes" "$(pv has_lookup)" check_eq $P/has-decode "yes" "$(pv has_decode)" check_eq $P/has-encode "yes" "$(pv has_encode)" check_eq $P/has-iter-decode "yes" "$(pv has_iter_decode)" check_eq $P/has-iter-encode "yes" "$(pv has_iter_encode)" check_eq $P/has-incremental-decoder "yes" "$(pv has_IncrementalDecoder)" check_eq $P/has-incremental-encoder "yes" "$(pv has_IncrementalEncoder)" check_eq $P/has-encoding-class "yes" "$(pv has_Encoding)" check_eq $P/has-utf8-constant "yes" "$(pv has_UTF8)" check_eq $P/has-labels "yes" "$(pv has_LABELS)" check_eq $P/has-python-names "yes" "$(pv has_PYTHON_NAMES)" check_eq $P/has-cache "yes" "$(pv has_CACHE)" check_eq $P/has-ascii-lower "yes" "$(pv has_ascii_lower)" check_eq $P/has-version-attr "yes" "$(pv has_VERSION)" # ---------------------------------------------------------------- the table --- # THE WHOLE LABEL TABLE, AS ONE DIGEST -- see the header for why this is not # twenty hand-written label cases. The counts beside it say which way a # mismatch moved; the sample cases below say what it is about. check_eq $P/labels-count "214" "$(pv labels_count)" check_eq $P/names-count "41" "$(pv names_count)" check_eq $P/labels-sha256 \ "f6769af8fabbb598ec95a41c557ef829c23f9448903c788b156be7e0c405fcff" \ "$(pv labels_sha256)" # The 41 canonical names in full. The digest above would catch any change to # this list too, but not READABLY: a reviewer looking at a red digest cannot see # what moved, and this is the case that shows them. check_eq $P/names-sorted \ "big5,euc-jp,euc-kr,gb18030,gbk,hz-gb-2312,ibm866,iso-2022-jp,iso-2022-kr,iso-8859-10,iso-8859-13,iso-8859-14,iso-8859-15,iso-8859-16,iso-8859-2,iso-8859-3,iso-8859-4,iso-8859-5,iso-8859-6,iso-8859-7,iso-8859-8,iso-8859-8-i,koi8-r,koi8-u,macintosh,shift_jis,utf-16be,utf-16le,utf-8,windows-1250,windows-1251,windows-1252,windows-1253,windows-1254,windows-1255,windows-1256,windows-1257,windows-1258,windows-874,x-mac-cyrillic,x-user-defined" \ "$(pv names_sorted)" # THE FOUR NAMES THE STANDARD SPELLS DIFFERENTLY FROM PYTHON. `Encoding.name' is # the STANDARD's name and the codec behind it is Python's; for these four they # differ, and the pairs below are what say the remap happened and did not leak # into the name. check_eq $P/python-names-count "4" "$(pv python_names_count)" check_eq $P/python-names \ "iso-8859-8-i>iso-8859-8,macintosh>mac-roman,windows-874>cp874,x-mac-cyrillic>mac-cyrillic" \ "$(pv python_names)" # EVERY CANONICAL NAME RESOLVES TO A CODEC ON THIS IMAGE. The one case here that # is about the PLATFORM rather than about the package -- see the header for what # was checked in the built artifacts and why this is still not a measurement. check_eq $P/codecs-missing "none" "$(pv codecs_missing)" # --------------------------------------------------------------- the lookup --- check_eq $P/lookup-utf8 "utf-8" "$(pv lookup_utf8)" check_eq $P/lookup-utf8-noh "utf-8" "$(pv lookup_utf8_noh)" check_eq $P/lookup-unicode11 "utf-8" "$(pv lookup_unicode11)" # THE FIVE ALIASES THE TICKET NAMES, and the answer is the point of the package: # every one of them is windows-1252, NOT latin-1. See the header. check_eq $P/lookup-latin1 "windows-1252" "$(pv lookup_latin1)" check_eq $P/lookup-iso88591 "windows-1252" "$(pv lookup_iso88591)" check_eq $P/lookup-cp1252 "windows-1252" "$(pv lookup_cp1252)" check_eq $P/lookup-windows1252 "windows-1252" "$(pv lookup_windows1252)" check_eq $P/lookup-ansi-x3 "windows-1252" "$(pv lookup_ansi_x3)" # ...and `ascii' and `us-ascii' land there too: the standard has no ASCII # encoding at all. The same rule from the other side, and the one most likely to # surprise a caller who thought they were asking for something narrow. check_eq $P/lookup-ascii "windows-1252" "$(pv lookup_ascii)" check_eq $P/lookup-us-ascii "windows-1252" "$(pv lookup_us_ascii)" # CASE IS FOLDED, AND ONLY OVER ASCII. `ascii_lower' lowercases through a utf-8 # round trip -- `bytes.lower()' touches only A-Z -- precisely so that a # non-ASCII character which `str.lower()' WOULD fold into the ASCII range does # not fold here. The Kelvin sign U+212A is the standard example, and the triple # below is the whole argument: `str.lower' turns it into a matching label, # `ascii_lower' does not, and so the lookup answers None where the lowered form # answers koi8-r. A suite that used `str.lower' here would have found nothing. check_eq $P/lookup-upper "utf-8" "$(pv lookup_upper)" check_eq $P/lookup-mixed "utf-8" "$(pv lookup_mixed)" check_eq $P/lookup-shift-jis-upper "shift_jis" "$(pv lookup_shift_jis_upper)" check_eq $P/ascii-lower-kelvin "'bac\\u212aground'" "$(pv ascii_lower_kelvin)" check_eq $P/str-lower-kelvin "'background'" "$(pv str_lower_kelvin)" check_eq $P/lookup-kelvin "None" "$(pv lookup_kelvin)" check_eq $P/lookup-kelvin-lowered "koi8-r" "$(pv lookup_kelvin_lowered)" # ASCII WHITESPACE IS STRIPPED, AND ONLY THESE FIVE: U+0009, U+000A, U+000C, # U+000D, U+0020. So a vertical tab (U+000B), which `str.strip()' WOULD have # stripped, is not -- and neither is a non-breaking space, which is what a label # copied out of a rendered page carries. Both are None, and that is the spec # being followed rather than a bug. check_eq $P/lookup-ws-all "utf-8" "$(pv lookup_ws_all)" check_eq $P/lookup-ws-tab "utf-8" "$(pv lookup_ws_tab)" check_eq $P/lookup-ws-vtab "None" "$(pv lookup_ws_vtab)" check_eq $P/lookup-ws-nbsp "None" "$(pv lookup_ws_nbsp)" # Stripping is at the ENDS only -- inner whitespace is not touched. check_eq $P/lookup-inner-space "None" "$(pv lookup_inner_space)" # THE UNKNOWN LABEL IS `None', NOT AN EXCEPTION -- see the header. The raising # entry points are `decode'/`encode', asserted below. check_eq $P/lookup-unknown "None" "$(pv lookup_unknown)" check_eq $P/lookup-empty "None" "$(pv lookup_empty)" check_eq $P/lookup-ws-only "None" "$(pv lookup_ws_only)" check_eq $P/lookup-utf9 "None" "$(pv lookup_utf9)" # A `bytes' label raises rather than answering None: `label.strip('\t\n\f\r ')' # is called before anything else and a bytes object will not take a str # argument. So the ONE input shape that gets neither a None nor a LookupError is # the one a caller who forgot to decode their header would pass. check_eq $P/lookup-bytes-raises "yes" "$(pv lookup_bytes_raises)" # THE ENCODING OBJECT IS CACHED AND SHARED. `lookup' memoises by canonical name, # so two different LABELS for one encoding answer the SAME object and a caller # may compare with `is'. A documented consequence of the CACHE dict that would # break silently if that went. check_eq $P/lookup-identity-same-label "yes" "$(pv lookup_identity_same_label)" check_eq $P/lookup-identity-alias "yes" "$(pv lookup_identity_alias)" # ...and the module-level `UTF8' constant is that same cached object, not a # second one built at import time. check_eq $P/lookup-is-utf8-constant "yes" "$(pv lookup_is_UTF8)" # `Encoding' defines __repr__, so its repr is stable and assertable -- unlike # the CodecInfo behind it, whose repr carries an address. That is why every # codec below is asserted by NAME. check_eq $P/encoding-repr "''" "$(pv encoding_repr)" check_eq $P/encoding-codec-name "utf-8" "$(pv encoding_codec_name)" # THE FOUR REMAPPED NAMES, each as a pair: the standard's name, then the Python # codec actually behind it. check_eq $P/remap-88598i-name "iso-8859-8-i" "$(pv remap_88598i_name)" check_eq $P/remap-88598i-codec "iso8859-8" "$(pv remap_88598i_codec)" check_eq $P/remap-maccyrillic-name "x-mac-cyrillic" "$(pv remap_maccyrillic_name)" check_eq $P/remap-maccyrillic-codec "mac-cyrillic" "$(pv remap_maccyrillic_codec)" check_eq $P/remap-macintosh-name "macintosh" "$(pv remap_macintosh_name)" check_eq $P/remap-macintosh-codec "mac-roman" "$(pv remap_macintosh_codec)" check_eq $P/remap-win874-name "windows-874" "$(pv remap_win874_name)" check_eq $P/remap-win874-codec "cp874" "$(pv remap_win874_codec)" # `x-user-defined' IS THE ONE ENCODING PYTHON DOES NOT HAVE -- see the header. # A red on `xud-not-in-codecs' would mean Python grew it and the remap should be # revisited; a red on a decode says this package's own charmap is broken. check_eq $P/xud-not-in-codecs "yes" "$(pv xud_not_in_codecs)" check_eq $P/xud-name "x-user-defined" "$(pv xud_name)" check_eq $P/xud-decode-low "'abc'" "$(pv xud_decode_low)" check_eq $P/xud-decode-80 "'\\uf780'" "$(pv xud_decode_80)" check_eq $P/xud-decode-ff "'\\uf7ff'" "$(pv xud_decode_ff)" # ALL 256 BYTES, ROUND-TRIPPED. A sample of three would pass with the charmap # table truncated anywhere past the third entry; this is the case that would not. check_eq $P/xud-encode-roundtrip "True" "$(pv xud_encode_roundtrip)" # --------------------------------------------------------------- the decode --- # NO BOM: THE DECLARED LABEL DECIDES, and the answer is a TUPLE -- the text and # the Encoding that produced it. A caller who treated the return value as a # string gets a tuple, so both halves are asserted separately throughout. check_eq $P/decode-plain-text "'abc'" "$(pv decode_plain_text)" check_eq $P/decode-plain-enc "utf-8" "$(pv decode_plain_enc)" # THE windows-1252 RULE, MEASURED AS A DECODE. 0x93 and 0x94 are the curly # quotes real web content means; the pair below is this package's answer and # Python's, side by side, and the difference between them is the whole reason # this package exists. check_eq $P/decode-latin1-text "'\\u201chi\\u201d'" "$(pv decode_latin1_text)" check_eq $P/decode-latin1-enc "windows-1252" "$(pv decode_latin1_enc)" check_eq $P/decode-python-iso88591 "'\\x93hi\\x94'" "$(pv decode_python_iso88591)" # THE BOM OVERRIDES THE DECLARED LABEL -- all three, each against a declared # label deliberately something else, so a green case cannot be the fallback # having been used by accident. check_eq $P/bom-utf8-text "'ab'" "$(pv bom_utf8_text)" check_eq $P/bom-utf8-enc "utf-8" "$(pv bom_utf8_enc)" check_eq $P/bom-utf16le-text "'ab'" "$(pv bom_utf16le_text)" check_eq $P/bom-utf16le-enc "utf-16le" "$(pv bom_utf16le_enc)" check_eq $P/bom-utf16be-text "'ab'" "$(pv bom_utf16be_text)" check_eq $P/bom-utf16be-enc "utf-16be" "$(pv bom_utf16be_enc)" # THE BOM IS CONSUMED, NOT DECODED. `utf-8-sig' would strip it too; `utf-16le' # would not, and that difference is what these two pin. check_eq $P/bom-stripped-utf8 "yes" "$(pv bom_stripped_utf8)" check_eq $P/bom-stripped-utf16le "yes" "$(pv bom_stripped_utf16le)" # A BOM-LOOKING PREFIX THAT IS NOT ONE. `\xff\xfe' is the utf-16le BOM, so # `\xff' alone is not: it falls through to the fallback and, under the default # error handler, becomes U+FFFD rather than raising. check_eq $P/bom-partial-text "'\\ufffd'" "$(pv bom_partial_text)" check_eq $P/bom-partial-enc "utf-8" "$(pv bom_partial_enc)" # THE DEFAULT ERROR HANDLER IS `replace', NOT `strict' -- the opposite of # `bytes.decode'. A caller who assumed the stdlib default gets no exception # where they expected one, and the three cases here are the three handlers side # by side so that the default is identified rather than described. check_eq $P/decode-bad-default "'a\\ufffdb'" "$(pv decode_bad_default)" check_eq $P/decode-bad-strict "yes" "$(pv decode_bad_strict)" check_eq $P/decode-bad-ignore "'ab'" "$(pv decode_bad_ignore)" # AN UNKNOWN LABEL RAISES HERE, where `lookup' returned None -- see the header. check_eq $P/decode-unknown-raises "yes" "$(pv decode_unknown_raises)" check_eq $P/encode-unknown-raises "yes" "$(pv encode_unknown_raises)" # ...and the label is resolved BEFORE the input is looked at, so an unknown # label raises even for input whose BOM names a known encoding. That is the # docstring's "fail early", and it is worth pinning because the BOM would # otherwise have made the fallback irrelevant. check_eq $P/decode-unknown-raises-with-bom "yes" "$(pv decode_unknown_raises_with_bom)" # AN Encoding OBJECT IS ACCEPTED WHERE A LABEL IS, which is what # `_get_encoding''s `hasattr(x, "codec_info")' is for: a caller that resolved # the label once may pass the object and skip the table. check_eq $P/decode-by-object "utf-8" "$(pv decode_by_object)" # --------------------------------------------------------------- the encode --- # THE DEFAULT ENCODING IS UTF-8 AND THE DEFAULT HANDLER IS `strict' -- the # opposite pair from `decode'. Both are asserted rather than described. check_eq $P/encode-default "b'h\\xc3\\xa9'" "$(pv encode_default)" check_eq $P/encode-latin1 "b'h\\xe9'" "$(pv encode_latin1)" check_eq $P/encode-by-object "b'h\\xe9'" "$(pv encode_by_object)" check_eq $P/encode-bad-strict "yes" "$(pv encode_bad_strict)" check_eq $P/encode-bad-replace "b'?'" "$(pv encode_bad_replace)" # NO BOM IS EVER WRITTEN. `encode' is the codec's encoder and nothing more, so a # round trip through `encode' then `decode' relies on the DECLARED label -- there # is no BOM to carry the encoding for it. Asymmetric with `decode', deliberately, # and the asymmetry is the thing a caller has to know. check_eq $P/encode-writes-no-bom "yes" "$(pv encode_writes_no_bom)" check_eq $P/encode-utf16le-no-bom "b'a\\x00'" "$(pv encode_utf16le_no_bom)" # ---------------------------------------------------------- the incremental --- # EVERY SPLIT OF A MULTI-BYTE FIXTURE AGREES WITH THE ONE-SHOT CALL -- see the # header for the fixture and why every offset rather than one. `-splits' is the # count driven and `-splits-ok' the count that agreed; asserting both is what # distinguishes "all twelve agreed" from "the loop ran zero times". check_eq $P/iter-decode-splits "12" "$(pv iter_decode_splits)" check_eq $P/iter-decode-splits-ok "12" "$(pv iter_decode_splits_ok)" check_eq $P/iter-decode-first-bad "none" "$(pv iter_decode_first_bad)" # The same twelve splits through the push-based IncrementalDecoder, which is the # class `iter_decode' is built on -- driven separately so a failure says which # of the two layers it is in. check_eq $P/incremental-splits-ok "12" "$(pv incremental_splits_ok)" check_eq $P/incremental-first-bad "none" "$(pv incremental_first_bad)" # The encoder, split over the TEXT rather than the bytes. check_eq $P/iter-encode-splits "6" "$(pv iter_encode_splits)" check_eq $P/iter-encode-splits-ok "6" "$(pv iter_encode_splits_ok)" check_eq $P/iter-encode-first-bad "none" "$(pv iter_encode_first_bad)" # THE BOM IS SNIFFED ACROSS CHUNKS -- the case the one-shot decoder cannot have. # Here the three BOM bytes arrive one at a time against a declared iso-8859-1, # and the answer is still utf-8. check_eq $P/iter-decode-bom-enc "utf-8" "$(pv iter_decode_bom_enc)" check_eq $P/iter-decode-bom-text "'ab'" "$(pv iter_decode_bom_text)" # ...AND A SHORT INPUT THAT NEVER REACHES THREE BYTES still resolves, because # `final=True' forces the decision. Without that the buffered bytes would be # dropped in silence -- one to two bytes of a document, gone with no error. check_eq $P/iter-decode-short "'a'" "$(pv iter_decode_short)" # An EMPTY input still names an encoding: the fallback, since no BOM can be # found in nothing. A caller reading the second half of the tuple gets an answer # rather than None. check_eq $P/iter-decode-empty "''" "$(pv iter_decode_empty)" check_eq $P/iter-decode-empty-enc "windows-1252" "$(pv iter_decode_empty_enc)" # The decoder's `encoding' attribute is None until it has decided, which is the # documented shape and the reason `iter_decode' yields the Encoding rather than # exposing the decoder. A caller that read it too early gets None, not a guess. check_eq $P/incremental-encoding-before "None" "$(pv incremental_encoding_before)" check_eq $P/incremental-encoding-after "utf-8" "$(pv incremental_encoding_after)" # -------------------------------------------------------- upstream's tests --- # THE AUTHOR'S OWN SUITE, RUN ON THIS IMAGE -- see the header for why it needs # neither pytest nor unittest, and why `test_all_labels' is the broadest single # thing that runs here. The COUNT is asserted, not just the failure total: a # collection that found nothing would report zero failures and read as green. check_eq $P/upstream-ran "yes" "$(pv upstream_ran)" check_eq $P/upstream-count "8" "$(pv upstream_count)" check_eq $P/upstream-names \ "test_all_labels,test_decode,test_encode,test_invalid_label,test_iter_decode,test_iter_encode,test_labels,test_x_user_defined" \ "$(pv upstream_names)" check_eq $P/upstream-failed "none" "$(pv upstream_failed)" rm -rf "$W"