. "$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 formats about ninety small values and opens # # two gettext catalogues, which is nothing beside the # # interpreter load. The three starts are `import', # # `datetime-import' and the probe -- this package ships no # # script, so there is no fourth. # # # # 1200 is 99-python3-jstyleson's and 99-python3-jsonpointer'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-humanize 4.9.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-humanize_4.9.0.bb, `inherit pypi # python_hatchling', PyPI sdist) on the wasm32-hwjs glibc image -- NUMBERS, BYTE # COUNTS AND DURATIONS RENDERED THE WAY A PERSON WOULD SAY THEM. `naturalsize', # `intcomma', `intword', `ordinal', `naturaldelta', `naturaltime', # `precisedelta'. # # ============================================================================ # WHY EVERY EXPECTED VALUE HERE IS A TRANSCRIBED LITERAL, WHICH IS UNUSUAL. # ============================================================================ # # 99-python3-jstyleson can state its package's definition as an EQUALITY -- the # stdlib parsing the same document written without the dialect -- because a # parser has an independent second reading. humanize has none. Its output is a # choice about ENGLISH: that 1000 bytes is `1.0 kB', that 12 is `12th' and not # `12nd', that 60 days is `a month'. There is no second implementation on this # image to compare against, so the control has to be a value somebody measured. # # What makes that safe is that every input is FIXED and every input that could # reach a clock is given a fixed reference instant instead. See the probe's # header: `naturaltime' is driven with `when=2024-01-15T12:00:00' and the values # are offsets computed from it, `naturaldelta' and `precisedelta' take a # `timedelta' outright, and `naturalday'/`naturaldate' are deliberately absent # because they compare against today with no injection point. # # ============================================================================ # WHAT WOULD FAIL A NO-OP INSTALL. # ============================================================================ # # A package with no second reading needs this said explicitly, because a suite of # literals could in principle be satisfied by an image that installed nothing at # all if the calls were somehow answered elsewhere. Three cases carry it: # # `import' -- there is no `humanize' in the stdlib and none in any other # package on this image, so the import is the deliverable. # # `dist-version' -- and this one is not decoration. `humanize/__init__.py' # ends with `__version__ = importlib.metadata.version(__name__)', so the # INSTALLED METADATA IS ON THE IMPORT PATH: a package whose dist-info did not # land raises PackageNotFoundError at import, before any formatter is reached. # The version is therefore asserted three ways -- the dist-info directory name, # `Version:' in METADATA, and the reading through importlib.metadata -- because # they are three independent statements of one fact the import depends on. # # `ru-translated' -- with the ru_RU catalogue active the answer must NOT be # the English one. A humanize that had degraded to something returning its # input, or an i18n layer that silently fell back, cannot pass that and the # English cases at once. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, NO EXTENSION, DEPENDENCIES ALREADY HERE. # ============================================================================ # # 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. # # humanize ships NO C: no .c, .pyx or .so in the sdist, and pyproject.toml # declares no ext_modules -- the build backend is `hatchling' with `hatch-vcs' # for the version, both NATIVE-only, so nothing about the backend reaches the # target. Its runtime imports are `datetime' and `gettext', both sub-packages of # the ported python3, and the recipe's RDEPENDS is `python3-datetime' and # `python3-setuptools', which this image already carries. So this port waits on # no other package. `pkg-no-so' below is a REGRESSION TRIPWIRE on a version bump # rather than a statement about today. # # ============================================================================ # THE ONE THING ABOUT THIS PACKAGE THAT COULD PLAUSIBLY BREAK ON THIS IMAGE. # ============================================================================ # # `activate("ru_RU")' is the case the ticket named, and it is worth saying # precisely what it is and is not. It is NOT setlocale(3) and it does not consult # LANG, LC_ALL or anything glibc knows about. It is # `gettext.translation("humanize", , ["ru_RU"])' -- # a lookup for a .mo file INSIDE site-packages. So the trimmed locale set on this # image is irrelevant to it, and a suite that expected a red there would be # expecting the wrong thing. # # What IS a real question is whether the catalogues shipped. Upstream GITIGNORES # the .mo files and regenerates them at build time; they reach the wheel only # because pyproject.toml names `[tool.hatch.build] artifacts = ["*.mo"]'. A build # that lost that line -- a version bump, a backend change -- would install a # humanize whose every formatter worked and whose activate() raised # FileNotFoundError. `mo-count', `ru-mo-present' and `ru-activate' are three # readings of that one fact, coarsest first, so a red names how far it got. # # `missing-locale' is the other half: activate() on a locale the package does NOT # carry must RAISE. Without it, a lookup that had degraded to returning # NullTranslations for everything would pass every English case -- the answers # would be "right" because nothing was ever translated. # # ============================================================================ # THE RUSSIAN ANSWERS ARE ASSERTED AS UTF-8 BYTES IN HEX, NOT AS CYRILLIC. # ============================================================================ # # This file is `ash' script read off a virtiofs root, and a suite's results cross # a serial console to the host. A Cyrillic literal here would be asserting the # whole of that transport alongside the catalogue, and a red would not say which # of the two moved. Each hex case below is commented with what it decodes to, and # the probe also emits the raw string as `*_raw' -- unscored -- so a person # reading a capture sees the words rather than a digest. # # ============================================================================ # WHAT IS NOT COVERED, AND WHY. # ============================================================================ # # The recipe does NOT `inherit ptest'. Upstream's sdist ships no tests directory # at all -- the GitHub repo has one, but `tests/' is outside the sdist -- so there # is no upstream suite to run here or anywhere. These cases are the only statement # about this package on this image. # # `naturalday' and `naturaldate' are absent for the reason given above: they # compare against TODAY with no `when=' to inject, so they cannot be stated as a # fixed pair. What they format is `strftime', which 99-python3 already covers. # # `apnumber', `fractional', `scientific', `clamp' and `metric' are exported and # are not driven. They are the same shape as the formatters that are -- pure # arithmetic over a fixed input -- so driving them would add vectors without # adding a distinguishable failure mode, and `api-*' below asserts each is bound. # # ============================================================================ # WHERE THE ASSERTED VALUES COME FROM. # ============================================================================ # # Every literal below was measured on x86-64 TWICE: against the sdist the # recipe's SRC_URI sha256 pins (582a265c...cfa, verified by digest before # measuring), and again against the module bytes unpacked from the built # python3-humanize_4.9.0-r0_wasm32.deb, which are byte-identical to the first for # all five .py files. The two runs agree on every key but the two path keys. # # 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. Nothing asserted # below is a documented 3.10/3.12 difference -- these are `%'-formatting and # integer division over fixed inputs -- but that is a reading of the source, not # a measurement, and the on-target result is what settles it. The on-target # result is NOT YET MEASURED: the batch verify is this port's first run on # wasm32-hwjs. P=python3-humanize # THE CANARY, which asserts nothing about python3-humanize. # # 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/humanize DI=$SITE/humanize-4.9.0.dist-info LOCALE=$PKGDIR/locale PKGLIST=/var/lib/dpkg/info/python3-humanize.list PROBE=$BST_DATA/python3-humanize-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-humanize.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import humanize' 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/package-dir test -d "$PKGDIR" check $P/module-init test -f "$PKGDIR/__init__.py" # ...AND IT IS A DIRECTORY, NOT A SINGLE MODULE. humanize splits its API across # filesize/number/time/i18n and re-exports them from `__init__' -- the opposite # shape from 99-python3-jstyleson's one file. Each submodule is named, because a # wheel that dropped one would still `import humanize' and would fail only at the # call, and `api-*' below would then be the first red rather than this. for _m in filesize i18n number time; do check $P/submodule-$_m test -f "$PKGDIR/$_m.py" done # `py.typed' is the PEP 561 marker. It is the one non-Python regular file the # package ships outside its metadata and its catalogues, so `pkg-only-python' # below exempts it by name; this case is what says the exemption is still about # a file that is actually there. check $P/py-typed test -f "$PKGDIR/py.typed" # 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, A CATALOGUE, OR 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. # # The exemptions are exactly the header's list: .po/.mo are the gettext # catalogues, `py.typed' is the PEP 561 marker. Anything else 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 ;; *.mo|*.po) continue ;; */py.typed) 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 ON THE IMPORT PATH. See the # header: `__init__.py' reads its own version through importlib.metadata, so a # missing dist-info is an ImportError and not a cosmetic gap. The directory name # carries the version, METADATA states it again, and `$P/dist-version' below # reads it the way the module itself does. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 4.9.0" cat "$DI/METADATA" # hatchling writes NO `top_level.txt' -- that is a setuptools artefact -- so # unlike 99-python3-jstyleson there is no such case here. Its absence is asserted # rather than left as an unexplained difference between the two suites, and a # backend change that started emitting one announces itself here. check_eq $P/no-top-level "absent" \ "$(if [ -f "$DI/top_level.txt" ]; then echo present; else echo absent; fi)" # -------------------------------------------------------------- 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-humanize is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -dbg or -dev # stanza of the same family. sed -n '/^Package: python3-humanize$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. Both were # already in this image before this port, which is why it waited on nothing -- # and each is asserted separately so a red names which one went missing rather # than reporting "the Depends line changed". 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-dt "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-st "python3-setuptools" cat "$W/dep" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If humanize 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 humanize' # ...and `datetime' separately, because half of humanize's API takes and returns # `datetime' objects and the recipe RDEPENDS on python3-datetime. A missing # python3-datetime presents AS a broken humanize -- the import above fails with a # traceback naming `humanize.time' -- and this case is what tells the two apart. check $P/datetime-import python3 -c 'import datetime' # ---------------------------------------------------------------- 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 `ru_ndelta_60s' # distinguishable from `ru_ndelta_60s_raw', and `intword_100' from # `intword_1000000'. 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, read the way the MODULE reads it. See the header: this is not a # third copy of `dist-info-version' for symmetry -- importlib.metadata over the # installed dist-info is literally what `humanize.__version__' evaluates, so this # case and the next are the import's own dependency, stated. check_eq $P/dist-version "4.9.0" "$(pv dist_version)" check_eq $P/version-attr "4.9.0" "$(pv version_attr)" # The module answered out of the directory 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 humanize. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # THE EXPORTED SURFACE, as the module's own `__all__' declares it -- not as dir() # reports it, which would include the submodule names `import' binds. A release # that dropped or renamed a formatter changes this line before it changes any # behaviour case below, so the red names the cause rather than the symptom. check_eq $P/api-count "19" "$(pv all_len)" check_eq $P/api-names \ '__version__,activate,apnumber,clamp,deactivate,decimal_separator,fractional,intcomma,intword,metric,naturaldate,naturalday,naturaldelta,naturalsize,naturaltime,ordinal,precisedelta,scientific,thousands_separator' \ "$(pv all_join)" # ------------------------------------------------ naturalsize: THREE SYSTEMS --- # DECIMAL is the default and divides by 1000. The pair 999/1000 is the boundary, # and it is the only place the arithmetic can be wrong in a way a mid-range value # would hide. check_eq $P/nsize-dec-0 "0 Bytes" "$(pv nsize_dec_0)" # ...and ONE byte is SINGULAR. A formatter built on a bare `%d Bytes' passes every # other decimal case and fails exactly this one. check_eq $P/nsize-dec-1 "1 Byte" "$(pv nsize_dec_1)" check_eq $P/nsize-dec-999 "999 Bytes" "$(pv nsize_dec_999)" check_eq $P/nsize-dec-1000 "1.0 kB" "$(pv nsize_dec_1000)" # 1023 AND 1024 ARE BOTH `1.0 kB' IN DECIMAL. This is the pair that separates the # decimal system from the binary one: an implementation that had silently taken # 1024 as its divisor would answer `1.0 kB' for 1024 and `1023 Bytes' for 1023, # passing one of these two and failing the other. check_eq $P/nsize-dec-1023 "1.0 kB" "$(pv nsize_dec_1023)" check_eq $P/nsize-dec-1024 "1.0 kB" "$(pv nsize_dec_1024)" check_eq $P/nsize-dec-3000 "3.0 kB" "$(pv nsize_dec_3000)" check_eq $P/nsize-dec-1e6 "1.0 MB" "$(pv nsize_dec_1000000)" check_eq $P/nsize-dec-3e6 "3.0 MB" "$(pv nsize_dec_3000000)" # ONE UNIT PER MAGNITUDE, up to a terabyte. The suffix table is walked by # repeated division, so a table that had lost an entry shifts every answer above # the gap and this is the case furthest from the boundary cases that would catch # it. check_eq $P/nsize-dec-1e12 "1.0 TB" "$(pv nsize_dec_1000000000000)" # BINARY divides by 1024 and spells the units KiB/MiB/GiB. The same boundary # again, one value either side. check_eq $P/nsize-bin-1023 "1023 Bytes" "$(pv nsize_bin_1023)" check_eq $P/nsize-bin-1024 "1.0 KiB" "$(pv nsize_bin_1024)" check_eq $P/nsize-bin-3000 "2.9 KiB" "$(pv nsize_bin_3000)" check_eq $P/nsize-bin-1Mi "1.0 MiB" "$(pv nsize_bin_1048576)" # A TERABYTE IN DECIMAL IS NOT A TEBIBYTE. `931.3 GiB' is the case that says the # two systems really do diverge rather than sharing a table with a different # suffix list -- and it is the one answer here a reader is likeliest to check by # hand. check_eq $P/nsize-bin-1e12 "931.3 GiB" "$(pv nsize_bin_1000000000000)" # GNU is `ls -sh' style: base 2**10 like binary, but a bare letter and NO SPACE. # The missing space is the whole difference from the binary form and is the part # an implementation sharing one formatter would get wrong. check_eq $P/nsize-gnu-300 "300B" "$(pv nsize_gnu_300)" check_eq $P/nsize-gnu-1023 "1023B" "$(pv nsize_gnu_1023)" check_eq $P/nsize-gnu-1024 "1.0K" "$(pv nsize_gnu_1024)" check_eq $P/nsize-gnu-3000 "2.9K" "$(pv nsize_gnu_3000)" check_eq $P/nsize-gnu-1Mi "1.0M" "$(pv nsize_gnu_1048576)" # THE SIGN is carried out of the magnitude and put back on the formatted result, # which is a separate code path from everything above. check_eq $P/nsize-negative "-4.0 KiB" "$(pv nsize_neg)" # THE PRECEDENCE, STATED. `gnu=True' IGNORES `binary' -- upstream's documented # behaviour, and the opposite of what the signature's argument order suggests. A # reader who assumed otherwise would read `nsize-gnu-3000' as ambiguous; this # case is what removes the ambiguity. check_eq $P/nsize-gnu-wins "2.9K" "$(pv nsize_gnu_binary_ignored)" # THE CUSTOM FORMAT is forwarded to the `%' operator. A signature that had # dropped the parameter would pass every case above and silently ignore it here. check_eq $P/nsize-format "2.930K" "$(pv nsize_fmt)" # ------------------------------------------------------- intcomma: SEPARATORS --- # Below a thousand there is nothing to separate, which is where the function # starts applying. check_eq $P/intcomma-100 "100" "$(pv intcomma_100)" check_eq $P/intcomma-1000 "1,000" "$(pv intcomma_1000)" check_eq $P/intcomma-10123 "10,123" "$(pv intcomma_10123)" check_eq $P/intcomma-10311 "10,311" "$(pv intcomma_10311)" check_eq $P/intcomma-1e6 "1,000,000" "$(pv intcomma_1000000)" check_eq $P/intcomma-1e9 "1,234,567,890" "$(pv intcomma_1234567890)" # A FLOAT KEEPS ITS FRACTIONAL PART UNSEPARATED. Grouping the digits after the # point is the classic way to get this wrong, and it is invisible on every # integer case above. check_eq $P/intcomma-float "1,234,567.25" "$(pv intcomma_float)" # A STRING INPUT takes a different branch. A version that lost it returns the # input unchanged, which reads as a pass to anything that only checks the type. check_eq $P/intcomma-str "1,234,567.1234" "$(pv intcomma_str)" # ndigits ROUNDS AND PADS, which is the parameter's whole purpose. check_eq $P/intcomma-ndigits "1,234,567.12" "$(pv intcomma_ndigits)" # --------------------------------------------------------- intword: MAGNITUDES --- # Below a million intword returns the number unchanged -- this case says where # the function starts applying, and it is the one an implementation with an # off-by-one in its table would fail first. check_eq $P/intword-100 "100" "$(pv intword_100)" check_eq $P/intword-1e6 "1.0 million" "$(pv intword_1000000)" check_eq $P/intword-1p2e6 "1.2 million" "$(pv intword_1200000)" # ...AND IT ROUNDS RATHER THAN TRUNCATES. 1290000 is `1.3 million', not `1.2'; # the two vectors above and this one separate the two strategies. check_eq $P/intword-1p29e6 "1.3 million" "$(pv intword_1290000)" check_eq $P/intword-1e9 "1.0 billion" "$(pv intword_1000000000)" check_eq $P/intword-2e9 "2.0 billion" "$(pv intword_2000000000)" # ONE NAME PER MAGNITUDE, on the SHORT SCALE. `1.0 trillion' for 10**12 is the # case that pins which scale this package speaks: on the long scale that number # is a billion, and an implementation that had adopted it would pass every case # below a billion and fail from here up. check_eq $P/intword-1e12 "1.0 trillion" "$(pv intword_1000000000000)" check_eq $P/intword-1e15 "1.0 quadrillion" "$(pv intword_1000000000000000)" check_eq $P/intword-1e18 "1.0 quintillion" "$(pv intword_1000000000000000000)" # THE CUSTOM FORMAT, forwarded the same way naturalsize's is. check_eq $P/intword-format "1.23 million" "$(pv intword_fmt)" # ------------------------------------------------------- ordinal: THE IRREGULARS --- # THE ENTIRE CONTENT OF THIS FUNCTION IS THE EXCEPTIONS ENGLISH MAKES. 11, 12 and # 13 take `th' where 1, 2 and 3 take `st', `nd' and `rd'; 21 goes back to `st'. # An implementation that keyed on the LAST DIGIT alone passes 1, 2, 3, 21, 22, 23 # and fails exactly 11, 12, 13 -- so this set is chosen to separate those two # implementations rather than to sample the range, and 101/111 repeat the # separation a hundred up, where a table indexed on two digits would break # instead. check_eq $P/ordinal-0 "0th" "$(pv ordinal_0)" check_eq $P/ordinal-1 "1st" "$(pv ordinal_1)" check_eq $P/ordinal-2 "2nd" "$(pv ordinal_2)" check_eq $P/ordinal-3 "3rd" "$(pv ordinal_3)" check_eq $P/ordinal-4 "4th" "$(pv ordinal_4)" check_eq $P/ordinal-11 "11th" "$(pv ordinal_11)" check_eq $P/ordinal-12 "12th" "$(pv ordinal_12)" check_eq $P/ordinal-13 "13th" "$(pv ordinal_13)" check_eq $P/ordinal-21 "21st" "$(pv ordinal_21)" check_eq $P/ordinal-22 "22nd" "$(pv ordinal_22)" check_eq $P/ordinal-23 "23rd" "$(pv ordinal_23)" check_eq $P/ordinal-101 "101st" "$(pv ordinal_101)" check_eq $P/ordinal-111 "111th" "$(pv ordinal_111)" # ------------------------------------------ naturaldelta: A DURATION, FUZZED --- # A `timedelta' IN, WORDS OUT, AND NO CLOCK ANYWHERE. Each vector sits either # side of one of the function's thresholds -- a second, a minute, an hour, a day, # a month (30.5 days), a year -- because between thresholds the answer is a # division and AT them it is a decision, and only the decisions can be wrong in a # way that is not a rounding error. # # Note what these say that naturaltime's do not: naturaldelta adds NO TENSE. # `30 seconds', not `30 seconds ago'. That is the documented difference between # the two functions and it is asserted by these two blocks standing side by side. check_eq $P/ndelta-0s "a moment" "$(pv ndelta_0s)" check_eq $P/ndelta-1s "a second" "$(pv ndelta_1s)" check_eq $P/ndelta-30s "30 seconds" "$(pv ndelta_30s)" # A MINUTE IS `a minute', NOT `1 minute'. The article is the whole style of this # package and it is applied at every unit's first multiple; an implementation # that formatted the number would pass `30 seconds' and fail here, at `an hour', # at `a day' and at `a year' together. check_eq $P/ndelta-60s "a minute" "$(pv ndelta_60s)" # 90 SECONDS IS STILL `a minute'. This is the fuzz, stated: naturaldelta rounds # DOWN to the unit it picked rather than reporting a minute and a half, which is # exactly what precisedelta is for and is asserted there. check_eq $P/ndelta-90s "a minute" "$(pv ndelta_90s)" check_eq $P/ndelta-1h "an hour" "$(pv ndelta_1h)" # ...and `an', not `a', before a vowel. A single hardcoded article passes every # other case on this list. check_eq $P/ndelta-2h "2 hours" "$(pv ndelta_2h)" check_eq $P/ndelta-1d "a day" "$(pv ndelta_1d)" check_eq $P/ndelta-2d "2 days" "$(pv ndelta_2d)" # 30 DAYS IS STILL DAYS AND 60 IS A MONTH. The month band starts at 30.5 days, so # this pair is the boundary -- and it is the one the `months' parameter switches # off, which `ndelta-nomonths' below states. check_eq $P/ndelta-30d "30 days" "$(pv ndelta_30d)" check_eq $P/ndelta-60d "a month" "$(pv ndelta_60d)" check_eq $P/ndelta-365d "a year" "$(pv ndelta_365d)" check_eq $P/ndelta-800d "2 years" "$(pv ndelta_800d)" # A BARE NUMBER OF SECONDS is accepted and must agree with the timedelta of the # same length. A version that lost the coercion returns the int unchanged, which # a type-blind caller would never notice. check_eq $P/ndelta-int "a minute" "$(pv ndelta_int)" # months=False replaces the month band with days. That is the parameter's whole # effect and it is invisible on every vector under 30.5 days, so 60 days is the # only place it can be stated. check_eq $P/ndelta-nomonths "60 days" "$(pv ndelta_nomonths)" # --------------------------------- naturaltime: THE SAME MAGNITUDES, WITH TENSE --- # AGAINST A FIXED REFERENCE INSTANT, WHICH IS WHAT MAKES THIS ASSERTABLE AT ALL. # `when=' is passed 2024-01-15T12:00:00 and every value is an offset computed # from it inside the probe, so neither the tense nor the magnitude can depend on # what the guest thinks the time is. Without it these cases would be asserting on # a moving target and would red the first time this image's clock -- which has no # RTC behind it -- disagreed with the host's. # # `when' is asserted first, so a probe that had defaulted it makes THAT the red # rather than making every case below look like a broken formatter. check_eq $P/ntime-when "2024-01-15T12:00:00" "$(pv when)" check_eq $P/ntime-past-30s "30 seconds ago" "$(pv ntime_past_30s)" check_eq $P/ntime-future-30s "30 seconds from now" "$(pv ntime_future_30s)" check_eq $P/ntime-past-90s "a minute ago" "$(pv ntime_past_90s)" check_eq $P/ntime-future-90s "a minute from now" "$(pv ntime_future_90s)" check_eq $P/ntime-past-2h "2 hours ago" "$(pv ntime_past_2h)" check_eq $P/ntime-future-2h "2 hours from now" "$(pv ntime_future_2h)" check_eq $P/ntime-past-1d "a day ago" "$(pv ntime_past_1d)" check_eq $P/ntime-future-1d "a day from now" "$(pv ntime_future_1d)" # BEYOND A YEAR naturaltime reports TWO units where naturaldelta reports one -- # `1 year, 1 month ago', not `a year ago'. That divergence between the two # functions is real and is the reason both are driven over the same magnitudes. check_eq $P/ntime-past-400d "1 year, 1 month ago" "$(pv ntime_past_400d)" check_eq $P/ntime-future-400d "1 year, 1 month from now" "$(pv ntime_future_400d)" # THE ZERO CASE IS ITS OWN WORD -- `now', not `a moment ago'. It is the one branch # naturaltime has that naturaldelta does not, and a wrapper that simply appended # the tense to naturaldelta's answer would fail exactly here. check_eq $P/ntime-now "now" "$(pv ntime_now)" # ------------------------------ precisedelta: EVERY UNIT THAT CARRIES SOMETHING --- # WHERE naturaldelta PICKS ONE UNIT AND FUZZES, precisedelta ENUMERATES. The # vector is deliberately awkward -- 2 days, 3633 seconds and 123000 microseconds # -- because a duration with several non-zero units at once is the only shape # that distinguishes the two functions, and the carrying between units is the # only arithmetic here that a simpler implementation would get wrong. check_eq $P/pd-default "2 days, 1 hour and 33.12 seconds" "$(pv pd_default)" # THE OXFORD-COMMA-FREE `and' BEFORE THE LAST UNIT is part of the contract: the # separator between the first units is `, ' and before the last it is ` and '. A # naive `", ".join' passes nothing here. # # THE CUSTOM FORMAT reaches only the FRACTIONAL unit, not the whole ones. `2 # days' does not become `2.0000 days'. check_eq $P/pd-format "2 days, 1 hour and 33.1230 seconds" "$(pv pd_fmt)" # minimum_unit=microseconds READJUSTS UPWARD to the largest unit that loses no # precision -- so the answer is MILLISECONDS, not microseconds, and the seconds # become whole. That readjustment is the documented behaviour and is the part a # reader would not predict from the parameter's name. check_eq $P/pd-microseconds "2 days, 1 hour, 33 seconds and 123 milliseconds" \ "$(pv pd_microseconds)" # SUPPRESSING A UNIT DOES NOT DROP ITS TIME -- it is carried into the next unit # down. 2 days and 1 hour become 49 hours, and the total is preserved. An # implementation that merely omitted the unit would answer `1 hour and 33.12 # seconds' and lose two days. check_eq $P/pd-suppress-days "49 hours and 33.12 seconds" "$(pv pd_suppress_days)" # ...and the same carrying in the other direction: with minutes as the floor, 90 # minutes is reported as an hour and thirty rather than as 90. check_eq $P/pd-minutes "1 hour and 30 minutes" "$(pv pd_minutes)" # THE SINGULAR, with no `and' and no fraction, which is the degenerate shape the # joining code has to handle without emitting a stray separator. check_eq $P/pd-one-second "1 second" "$(pv pd_one_second)" # ------------------------------------------------------------------ i18n ------ # THE CATALOGUES SHIPPED. See the header: the .mo files are gitignored upstream # and reach the wheel only through `[tool.hatch.build] artifacts = ["*.mo"]', so # this is a real question about the build rather than a formality. Three readings # of the one fact, coarsest first, so a red says how far it got. check $P/locale-dir test -d "$LOCALE" check_eq $P/mo-count "33" "$(pv mo_count)" check_eq $P/ru-mo-present "yes" "$(pv ru_mo_present)" check $P/ru-mo-file test -f "$LOCALE/ru_RU/LC_MESSAGES/humanize.mo" # ...and the count read off the ROOTFS as well as out of the probe. The two would # disagree only if the probe had answered out of a different copy of the package # than the one `module-answered' pins, which is worth being able to tell. check_eq $P/mo-count-rootfs "33" \ "$(find "$LOCALE" -name humanize.mo 2>/dev/null | wc -l | tr -d ' ')" # activate() DID NOT RAISE... check_eq $P/ru-activate "yes" "$(pv ru_activate)" # ...AND THE CATALOGUE ACTUALLY ANSWERED. An activate that returned without # raising would pass the case above while translating nothing, so the assertion # has to be on the TRANSLATED TEXT. Hex-encoded UTF-8, for the transport reason # in the header; each decodes to the Russian in the comment. # # d0bc d0b8 d0bd d183 d182 d183 = "минуту" (a minute, accusative) check_eq $P/ru-ndelta-60s "d0bcd0b8d0bdd183d182d183" "$(pv ru_ndelta_60s)" # d0b4 d0b5 d0bd d18c = "день" (a day) check_eq $P/ru-ndelta-1d "d0b4d0b5d0bdd18c" "$(pv ru_ndelta_1d)" # "2 часа назад" -- note the ASCII `2 ' at the front (32 20) and that the TENSE # suffix is translated too, which is what says naturaltime's own strings come # from the catalogue and not only naturaldelta's. check_eq $P/ru-ntime-2h "3220d187d0b0d181d0b020d0bdd0b0d0b7d0b0d0b4" \ "$(pv ru_ntime_2h)" # THE SEMANTIC POINT, stated in a form that survives upstream revising the # wording: with ru_RU active the answer is NOT the English one. This is the third # of the three cases that fail a no-op install (see the header). check_eq $P/ru-translated "yes" "$(pv ru_translated)" # deactivate() RESTORES IT. This is the half a suite driving only activate would # miss: a deactivate that did not reset the thread-local would leave every later # caller in Russian, and the failure would surface far from here -- in whichever # unrelated code formatted the next duration. check_eq $P/ru-deactivate "yes" "$(pv ru_deactivate)" check_eq $P/post-ndelta-60s "a minute" "$(pv post_ndelta_60s)" check_eq $P/post-ndelta-1d "a day" "$(pv post_ndelta_1d)" # A LOCALE THE PACKAGE DOES NOT CARRY MUST RAISE. Without this, a lookup that had # degraded to returning NullTranslations for everything would pass every English # case above -- the answers would be "right" because nothing was ever translated. # The exception TYPE is asserted rather than just the fact of one, because # FileNotFoundError is what says gettext looked for a file and did not find it, # as opposed to failing before it got that far. check_eq $P/missing-locale "FileNotFoundError" "$(pv missing_locale)" # THE SEPARATORS ARE A PLAIN DICT, NOT A CATALOGUE. `thousands_separator()' and # `decimal_separator()' read `_THOUSANDS_SEPARATOR' / `_DECIMAL_SEPARATOR' in # i18n.py and never open a .mo, so they work on an image that shipped no # catalogues at all. That is why they are asserted separately from the ru cases: # together, the two blocks say whether a red is in the locale LOOKUP or in the # CATALOGUE. check_eq $P/de-thousands "." "$(pv de_thousands)" check_eq $P/de-decimal "," "$(pv de_decimal)" # ------------------------------------------------------------------------------ # WHICH INTERPRETER ANSWERED. The literals above were measured on CPython 3.10 # and this image ships 3.12 (see the header). Nothing asserted is a documented # difference between the two, but that is a reading of the source rather than a # measurement -- so the version is recorded here, and a red anywhere above is # read next to it. check_eq $P/py-version "3.12" "$(pv py_version)" # 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"