. "$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 and parses about seventy small values, # # which is nothing beside the interpreter load. The three # # starts are `import', the probe, and the one CLI run -- # # this package ships a console script, so unlike # # 99-python3-jstyleson there IS a third. # # # # 1200 is 99-python3-jstyleson'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-humanfriendly 10.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-humanfriendly_10.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image -- NUMBERS RENDERED TO # TEXT A PERSON CAN READ, AND THAT TEXT READ BACK TO A NUMBER. `format_size', # `format_number', `format_length' and `format_timespan' in one direction; # `parse_size', `parse_length' and `parse_timespan' in the other. # # ============================================================================ # THE PARSERS ARE WHAT MAKES THIS TESTABLE, AND WHY. # ============================================================================ # # A formatter on its own can only be asserted against literals somebody # transcribed, and a transcription error looks exactly like a working library. The # parsers give a SECOND, INDEPENDENT statement about the same units, and the two # together give a ROUND TRIP -- format a value, read it back, get the value -- # whose right-hand side nobody typed. `roundtrip-size-exact' and its siblings are # the cases that survive an editing mistake in the tables above them. # # THE ROUND TRIP IS NOT UNIVERSAL, AND THAT IS MEASURED. format_size rounds to two # decimals, so 1024 renders `1.02 KB' and reads back 1020. The round trip is # therefore asserted over the values that format EXACTLY, and the lossy case is # asserted as its own measured pair (`roundtrip-size-lossy-*') rather than left as # a gap a later reader would have to rediscover. Same shape for timespans, where # the restriction is sharper: `1 hour and 30 minutes' does not parse at all. # # ============================================================================ # TWO NAMES THIS TICKET PREDICTED DO NOT EXIST IN 10.0. # ============================================================================ # # BRINTOS-1956's description names `format_table' and `terminal_size()'. Neither # is in the package. The real names, measured against the sdist the recipe pins, # are `humanfriendly.tables.format_pretty_table' and # `humanfriendly.terminal.find_terminal_size'. The suite drives the real ones and # asserts the ABSENCE of the predicted ones (`no-terminal-size-name', # `no-format-table-name'), so a release that grew either announces itself here # instead of leaving the port's documentation quietly wrong. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, NO C, NO EXTERNAL DEPENDENCY. # ============================================================================ # # 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. # # humanfriendly ships NO C: no .c, .pyx or .so in the sdist, and setup.py declares # `packages=find_packages()' with no ext_modules at all. Its `install_requires' is # EMPTY on Linux/Python 3 -- the only conditional entries are `pyreadline3' under # `sys_platform == "win32"' and `monotonic' under Python 2.7, and this image is # neither. Every name in the recipe's RDEPENDS is a sub-package of the ported # python3 (BRINTOS-1476), 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 PACKAGE DIRECTORY WITH A SUBPACKAGE, not a single module -- the opposite # shape from 99-python3-jstyleson and 99-python3-jsonpointer. `humanfriendly/' # holds twelve modules and `humanfriendly/terminal/' is itself a package with # three more. `terminal-is-package' asserts it, because a release that flattened # the layout would still import and still pass every behavioural case while the # shape this image ships had changed. # # ============================================================================ # THE CONSOLE SCRIPT IS PART OF THE PACKAGE, AND IS ASSERTED AS ONE CASE. # ============================================================================ # # setup.py declares `console_scripts=['humanfriendly = humanfriendly.cli:main']', # so the package installs /usr/bin/humanfriendly. It is a Python script with a # `#!/usr/bin/env python3' shebang -- an eight-line wrapper, not a binary -- which # is why `pkg-only-python' below has to exempt it BY PATH rather than by # extension. One CLI run (`cli-format-size') is asserted, because a console script # that the packaging dropped or left non-executable is a real regression and no # amount of `import humanfriendly' would catch it. # # ============================================================================ # WHAT IS NOT COVERED. # ============================================================================ # # The recipe does NOT `inherit ptest'. Upstream ships `humanfriendly/tests.py' and # it IS installed here, but running it needs `coloredlogs' (BRINTOS-1955, a # sibling in this batch) and drives interactive prompts and a spinner against a # real terminal, so it is not what this suite runs. These cases are the statement # about this package on this image. # # The INTERACTIVE half of the package -- `humanfriendly.prompts' (prompt_for_choice # and friends) and the terminal spinner -- is not driven at all. Both require a tty # and a person; there is no follow-on ticket under BRINTOS-780 for them, because # BRINTOS-1956 closes on this suite by design. `find_terminal_size' IS driven, in # the non-interactive form the header below describes. # # ============================================================================ # WHERE THE ASSERTED VALUES COME FROM. # ============================================================================ # # Every literal below was measured on x86-64 CPython 3.12 against the sdist the # recipe's SRC_URI sha256 pins (6b0b831c...ddc, verified by digest before # measuring), and the payload cases against the bytes of the built # python3-humanfriendly_10.0-r0_wasm32.deb. The control interpreter is the SAME # 3.12 the image ships, so unlike 99-python3-jstyleson there is no interpreter # version gap to state. The on-target result is not yet measured -- the batch # verify is this port's first run on wasm32-hwjs. P=python3-humanfriendly # THE CANARY, which asserts nothing about python3-humanfriendly. # # 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/humanfriendly DI=$SITE/humanfriendly-10.0.dist-info CLI=/usr/bin/humanfriendly PKGLIST=/var/lib/dpkg/info/python3-humanfriendly.list PROBE=$BST_DATA/python3-humanfriendly-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-humanfriendly.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import humanfriendly' 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/package-init test -f "$PKGDIR/__init__.py" # ...AND `terminal' IS A SUBPACKAGE, not a module. See the header: a release that # flattened it to `terminal.py' would still import and still pass every # behavioural case below, while the shape this image ships had changed. check $P/terminal-subpackage test -f "$PKGDIR/terminal/__init__.py" # The three modules the behavioural cases below actually reach into, named so a # missing one reds here rather than as an unexplained import error inside the # probe. check $P/module-tables test -f "$PKGDIR/tables.py" check $P/module-text test -f "$PKGDIR/text.py" check $P/module-cli test -f "$PKGDIR/cli.py" # THE CONSOLE SCRIPT, as a file and as something the shell may run. `cli-*' below # runs it; these say whether a failure there is about the script's absence, its # mode, or its behaviour. check $P/cli-present test -f "$CLI" check $P/cli-executable test -x "$CLI" # 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, ITS METADATA, OR THE ONE CONSOLE # SCRIPT. 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. # # /usr/bin/humanfriendly is exempted BY PATH and not by extension, because a # console script has none: it is an eight-line Python wrapper with a # `#!/usr/bin/env python3' shebang. Exempting it by pattern would exempt any # future binary landing beside it. # # 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 "$CLI") continue ;; *.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. Unlike 99-python3-jstyleson this package ALSO carries a # `__version__' in the module, so the version is stated twice and independently -- # `mod-version' and `dist-version' below cross-check them, and a wheel that landed # beside a different module tree would import fine and disagree there. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 10.0" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import humanfriendly' has to match, and a wheel that renamed the package would # change it here first. check_out $P/dist-info-top-level "humanfriendly" cat "$DI/top_level.txt" # entry_points.txt is where the console script comes FROM. `cli-present' asserts # the file landed; this asserts the packaging still declares why it should. check_out $P/dist-info-entry-point "humanfriendly = humanfriendly.cli:main" \ cat "$DI/entry_points.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-humanfriendly 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 -dbg or -dev # stanza of the same family. sed -n '/^Package: python3-humanfriendly$/,/^$/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. Every one # is a sub-package of the ported python3 (BRINTOS-1476), which is why this port # waits on nothing; python3-core comes with the packaging. They are asserted one # per case rather than as one line, because a dropped RDEPENDS entry presents on # target as an ImportError deep inside the library and this is where it is # cheapest to name. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-core python3-datetime python3-fcntl python3-io python3-logging \ python3-math python3-numbers python3-shell python3-stringold; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If humanfriendly 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 humanfriendly' # ---------------------------------------------------------------- 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 `psize_42' distinguishable # from `psize_bin_42', and `span_1' from `span_1500'. 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, TWICE. `dist_version' is read through importlib.metadata over the # installed dist-info -- the reading a caller would get -- and `mod_version' is # the module's own `__version__'. A wheel that landed beside a different module # tree would import fine and disagree between these two. check_eq $P/dist-version "10.0" "$(pv dist_version)" check_eq $P/mod-version "10.0" "$(pv mod_version)" # The module answered out of the package 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 humanfriendly. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # THE PUBLIC NAMES, one case each -- the seven functions and the three exception # types this port promises. for _n in format_size format_number format_length format_timespan \ parse_size parse_length parse_timespan \ InvalidSize InvalidLength InvalidTimespan; do check_eq $P/api-$_n "yes" "$(pv has_$_n)" done # `humanfriendly.terminal' is a PACKAGE, asserted from the interpreter's side to # match `terminal-subpackage' above from the filesystem's. check_eq $P/terminal-is-package "yes" "$(pv terminal_is_package)" # ------------------------- the two names this ticket predicted and 10.0 lacks -- # See the header. The real names are asserted present and the predicted ones # absent, so a release that grew either announces itself here rather than leaving # the port's documentation quietly wrong. check_eq $P/api-find-terminal-size "yes" "$(pv has_find_terminal_size)" check_eq $P/no-terminal-size-name "no" "$(pv has_terminal_size_name)" check_eq $P/api-format-pretty-table "yes" "$(pv has_format_pretty_table)" check_eq $P/no-format-table-name "no" "$(pv has_format_table_name)" # ------------------------------------------------- format_size, both bases --- # The inputs are chosen so each names one rule: the singular/plural switch at 1, # the unit boundary at 1000 decimally and 1024 in binary, and the values where the # two bases DISAGREE. 1000 is the clearest of those -- `1 KB' decimally and # `1000 bytes' in binary -- and is the single case that says the `binary' flag is # wired at all. check_eq $P/size-dec-0 "0 bytes" "$(pv size_dec_0)" check_eq $P/size-dec-1 "1 byte" "$(pv size_dec_1)" check_eq $P/size-dec-512 "512 bytes" "$(pv size_dec_512)" check_eq $P/size-dec-999 "999 bytes" "$(pv size_dec_999)" check_eq $P/size-dec-1000 "1 KB" "$(pv size_dec_1000)" check_eq $P/size-dec-1024 "1.02 KB" "$(pv size_dec_1024)" check_eq $P/size-dec-1000000 "1 MB" "$(pv size_dec_1000000)" check_eq $P/size-dec-1048576 "1.05 MB" "$(pv size_dec_1048576)" check_eq $P/size-dec-1500000000 "1.5 GB" "$(pv size_dec_1500000000)" check_eq $P/size-bin-0 "0 bytes" "$(pv size_bin_0)" check_eq $P/size-bin-1 "1 byte" "$(pv size_bin_1)" check_eq $P/size-bin-512 "512 bytes" "$(pv size_bin_512)" check_eq $P/size-bin-999 "999 bytes" "$(pv size_bin_999)" check_eq $P/size-bin-1000 "1000 bytes" "$(pv size_bin_1000)" check_eq $P/size-bin-1024 "1 KiB" "$(pv size_bin_1024)" check_eq $P/size-bin-1000000 "976.56 KiB" "$(pv size_bin_1000000)" check_eq $P/size-bin-1048576 "1 MiB" "$(pv size_bin_1048576)" check_eq $P/size-bin-1500000000 "1.4 GiB" "$(pv size_bin_1500000000)" # `keep_width' pads the fraction rather than dropping a trailing `.00'. It is the # one formatting option driven here, because it is the one that changes the TEXT # of an otherwise identical value -- it is what makes a column of sizes line up. check_eq $P/size-keepwidth "1.00 KB" "$(pv size_keepwidth_1000)" check_eq $P/size-keepwidth-bin "1.00 KiB" "$(pv size_keepwidth_bin_1024)" # ----------------------------------------------------------- format_number --- # Thousands separators, and TWO decimals by default. check_eq $P/number-0 "0" "$(pv number_0)" check_eq $P/number-1 "1" "$(pv number_1)" check_eq $P/number-1234 "1,234" "$(pv number_1234)" check_eq $P/number-6789000 "6,789,000" "$(pv number_6789000)" check_eq $P/number-1000000 "1,000,000" "$(pv number_1000000)" # THE FRACTION IS TRUNCATED, NOT ROUNDED. 1234.567 is `1,234.56' and not # `1,234.57'; 1234.565 is `1,234.56' too. This is the one value here a reader # would guess wrong, and the pair is what states it as truncation rather than as # a rounding mode that happens to agree once. check_eq $P/number-truncates "1,234.56" "$(pv number_trunc)" check_eq $P/number-truncates-up "1,234.56" "$(pv number_trunc_up)" check_eq $P/number-decimals-0 "1,234" "$(pv number_decimals_0)" check_eq $P/number-decimals-4 "1,234.567" "$(pv number_decimals_4)" # The sign survives a rounding-to-zero: -0.5 at zero decimals is `-0', not `0'. # The one case where the grouping code keeps something the arithmetic threw away. check_eq $P/number-negative-zero "-0" "$(pv number_negative_zero)" # ----------------------------------------------------------- format_length --- # Metres in, and the unit picked by magnitude: mm, cm, m, km. The singular/plural # switch is irregular here -- `1 metre', `42 metres' -- so it is asserted rather # than inferred from the size cases above. check_eq $P/length-0 "0 metres" "$(pv length_0)" check_eq $P/length-1mm "1 mm" "$(pv length_0_001)" check_eq $P/length-50cm "50 cm" "$(pv length_0_5)" check_eq $P/length-1 "1 metre" "$(pv length_1)" check_eq $P/length-42 "42 metres" "$(pv length_42)" check_eq $P/length-1km "1 km" "$(pv length_1000)" check_eq $P/length-1_5km "1.5 km" "$(pv length_1500)" # --------------------------------------------------------- format_timespan --- # Seconds rendered as a sentence. The multi-unit forms are the interesting ones: # they are exactly what parse_timespan cannot read back, which # `roundtrip-span-multi-raises' below records. check_eq $P/span-0 "0 seconds" "$(pv span_0)" check_eq $P/span-1 "1 second" "$(pv span_1)" check_eq $P/span-60 "1 minute" "$(pv span_60)" check_eq $P/span-90 "1 minute and 30 seconds" "$(pv span_90)" check_eq $P/span-3600 "1 hour" "$(pv span_3600)" check_eq $P/span-5400 "1 hour and 30 minutes" "$(pv span_5400)" check_eq $P/span-86399 "23 hours, 59 minutes and 59 seconds" "$(pv span_86399)" check_eq $P/span-86400 "1 day" "$(pv span_86400)" check_eq $P/span-604800 "1 week" "$(pv span_604800)" # `max_units' truncates the sentence rather than rounding it: the seconds are # DROPPED, not carried into the minutes. Compare with `span-86399' above. check_eq $P/span-max-units "23 hours and 59 minutes" "$(pv span_max_units_2)" # SUB-SECOND VALUES. Without `detailed', anything under a second that is not a # clean fraction reads `0 seconds' -- which looks like a bug and is not, so it is # pinned. `detailed' is what makes milliseconds appear at all. check_eq $P/span-fractional "1.5 seconds" "$(pv span_fractional)" check_eq $P/span-detailed "1 second and 500 milliseconds" "$(pv span_detailed)" check_eq $P/span-millisecond "0 seconds" "$(pv span_millisecond)" # --------------------------------------------------------------- parse_size --- # The other direction, against literals. A BARE NUMBER parses as the base unit -- # bytes here -- which is the rule that makes `parse_size("42")' 42 and not an # error. The `bin_' variants are the same input read with `binary=True', and the # pair at `1 KB' is what says the flag reaches the parser too: 1000 against 1024. check_eq $P/psize-42 "42" "$(pv psize_42)" check_eq $P/psize-0 "0" "$(pv psize_0)" check_eq $P/psize-1KB "1000" "$(pv psize_1_KB)" check_eq $P/psize-1KB-bin "1024" "$(pv psize_bin_1_KB)" check_eq $P/psize-1KiB "1024" "$(pv psize_1_KiB)" # ...and an EXPLICIT IEC unit is 1024-based in BOTH modes. `binary=True' changes # how an ambiguous `KB' is read; it does not change what `KiB' means. check_eq $P/psize-1KiB-bin "1024" "$(pv psize_bin_1_KiB)" check_eq $P/psize-5MB "5000000" "$(pv psize_5_MB)" check_eq $P/psize-5MB-bin "5242880" "$(pv psize_bin_5_MB)" check_eq $P/psize-5MiB "5242880" "$(pv psize_5_MiB)" check_eq $P/psize-1_5GB "1500000000" "$(pv psize_1_5_GB)" check_eq $P/psize-1TB "1000000000000" "$(pv psize_1_TB)" # ------------------------------------------------------------- parse_length --- check_eq $P/plength-42 "42" "$(pv plength_42)" check_eq $P/plength-1km "1000" "$(pv plength_1_km)" check_eq $P/plength-1mm "0.001" "$(pv plength_1_mm)" check_eq $P/plength-1cm "0.01" "$(pv plength_1_cm)" check_eq $P/plength-2_5m "2.5" "$(pv plength_2_5_m)" # ----------------------------------------------------------- parse_timespan --- # Note the return type: parse_timespan answers a FLOAT even for whole seconds, # where parse_size and parse_length answer an int. That difference is visible in # these literals and is upstream's, not this platform's. check_eq $P/pspan-42 "42.0" "$(pv pspan_42)" check_eq $P/pspan-1s "1.0" "$(pv pspan_1s)" check_eq $P/pspan-1minute "60.0" "$(pv pspan_1_minute)" check_eq $P/pspan-2h "7200.0" "$(pv pspan_2h)" check_eq $P/pspan-1day "86400.0" "$(pv pspan_1_day)" check_eq $P/pspan-1_5hours "5400.0" "$(pv pspan_1_5_hours)" # ---------------------------------------------- THE ROUND TRIP, AS A PROPERTY -- # Format a value, read it back, get the value. The right-hand side is not a # literal anybody typed, so these survive an editing mistake in every table above # -- and they are what say the formatter and the parser are talking about the # same units. Computed in the probe over the values that format EXACTLY; see the # header for why that restriction is real. check_eq $P/roundtrip-size-exact "yes" "$(pv roundtrip_size_exact)" check_eq $P/roundtrip-size-binary "yes" "$(pv roundtrip_size_binary)" check_eq $P/roundtrip-length "yes" "$(pv roundtrip_length)" check_eq $P/roundtrip-span-single "yes" "$(pv roundtrip_span_single)" # THE LOSSY CASE, STATED AS A VALUE RATHER THAN LEFT AS A GAP. format_size rounds # to two decimals, so 1024 renders `1.02 KB' and reads back 1020. Not a failure -- # a documented consequence -- and asserting it is what makes the exact set above a # restriction that was measured rather than one that was hoped for. check_eq $P/roundtrip-size-lossy-text "1.02 KB" "$(pv roundtrip_size_lossy_text)" check_eq $P/roundtrip-size-lossy-back "1020" "$(pv roundtrip_size_lossy_back)" # ...AND THE TIMESPAN RESTRICTION, WHICH IS SHARPER. format_timespan(5400) is # `1 hour and 30 minutes' and parse_timespan REFUSES it -- the tokenizer reads # `hour and' as one unit name. Upstream's behaviour on x86-64, asserted as its own # case because a caller who round-trips a duration will meet it and a silence here # would not warn them. check_eq $P/roundtrip-span-multi-raises "yes" "$(pv roundtrip_span_multi_raises)" # ----------------------------- THE DOCUMENTED FAILURE PATH: IT RAISES --------- # What separates a parser from a number scraper. `1 XB' has a unit that does not # exist, so parse_size must REFUSE it rather than answer 1. check_eq $P/psize-bad-unit-raises "yes" "$(pv psize_bad_unit_raises)" check_eq $P/psize-empty-raises "yes" "$(pv psize_empty_raises)" check_eq $P/psize-words-raises "yes" "$(pv psize_words_raises)" # ...and the same for the other two parsers, so a red names WHICH parser lost its # validation rather than leaving it to be inferred from parse_size alone. check_eq $P/plength-bad-unit-raises "yes" "$(pv plength_bad_unit_raises)" check_eq $P/pspan-bad-unit-raises "yes" "$(pv pspan_bad_unit_raises)" # `5 bogus' IS NOT THE MALFORMED CASE, and this is the case that says so. # parse_size answers 5: the tokenizer reads a bare number followed by an unknown # word as a byte count. Measured, not assumed -- a suite that asserted a raise # here would be asserting something upstream never promised, and would red on a # correct library. check_eq $P/psize-trailing-word "5" "$(pv psize_trailing_word)" # THE EXCEPTION TYPE IS PART OF THE CONTRACT, and it is NOT a ValueError. A caller # that wrote `except ValueError' around parse_size would catch nothing; this pair # says which handler actually works, and would announce a base-class change that # every `except' in every caller depends on. check_eq $P/invalid-size-base "Exception" "$(pv invalid_size_base)" check_eq $P/invalid-size-not-valueerror "no" "$(pv invalid_size_is_valueerror)" # ------------------------------------------- the terminal helpers, non-interactive # format_pretty_table over fixed rows. The output is a block of text, so it is # asserted three ways: by digest (exact), by line count (structure), and by two # lines quoted whole (legible). A release that changed the box-drawing moves the # digest while the line count holds; one that changed the padding moves both while # the header holds -- so a red says WHICH changed rather than only that something # did. check_eq $P/table-sha "01d13f8c05d2270f" "$(pv table_sha)" check_eq $P/table-len "258" "$(pv table_len)" check_eq $P/table-lines "7" "$(pv table_lines)" check_eq $P/table-rule "------------------------------------" "$(pv table_rule)" check_eq $P/table-header "| Interface | Traffic | Uptime |" "$(pv table_header)" check_eq $P/table-row0 "| eth0 | 1.5 GB | 3 hours |" "$(pv table_row0)" # find_terminal_size() AND ITS DOCUMENTED FALLBACK. The function tries ioctl(2) # TIOCGWINSZ on each standard stream, then the external `stty size', then falls # back to (DEFAULT_LINES, DEFAULT_COLUMNS) = (25, 80). # # NONE OF THAT IS DETERMINISTIC UNDER BST -- stdin may be a console in the guest # and is a pipe on the control host -- so the assertable case is the FALLBACK, and # the probe FORCES it rather than hoping for it: every standard stream is pointed # at /dev/null (no TIOCGWINSZ) and PATH is emptied (no `stty'). That is why these # two cases can carry literals at all. # # Note the order: (lines, columns), not the other way round. check_eq $P/terminal-default-lines "25" "$(pv default_lines)" check_eq $P/terminal-default-columns "80" "$(pv default_columns)" check_eq $P/terminal-fallback-lines "25" "$(pv terminal_fallback_lines)" check_eq $P/terminal-fallback-columns "80" "$(pv terminal_fallback_columns)" # The UNFORCED call is asserted as a SHAPE only -- two positive integers. Pinning # its value would be pinning whatever terminal the verify happened to attach, # which is a property of the harness rather than of this package. What this does # catch is the call raising, or answering something that is not a pair. check_eq $P/terminal-live-shape "yes" "$(pv terminal_live_shape)" # 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)" # ------------------------------------------------------------- the console script # THE THIRD AND LAST INTERPRETER START. The package ships /usr/bin/humanfriendly # (see the header); `cli-present' and `cli-executable' above say the file landed # and is runnable, and this says it WORKS. A console script the packaging dropped, # left non-executable, or wired to a missing entry point is a real regression that # no amount of `import humanfriendly' would catch. # # One invocation, not a table: the CLI is a thin wrapper over format_size, which # every case above has already driven from the library side, so a second run would # cost another interpreter start to re-measure the same function. check_out $P/cli-format-size "1 KB" "$CLI" --format-size=1000 rm -rf "$W"