. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and that is the whole cost. One is the # # bare `import' case, one is the probe; everything else is a # # shell comparison against the probe's transcript, a read of # # dpkg's own records, or the single run of the shipped # # console script -- which is a third interpreter start, but # # of the same static image and after the other two have # # already paged it in. It matches 99-python3-colorama's 1200 # # for the same reason that suite matches 99-python3-tomli-w's: # # the start dominates and a further start is not a further # # suite. NOT MEASURED ON TARGET YET -- this port's first # # on-target run is the batch verify, and the measured elapsed # # belongs in the test plan rather than in this comment, which # # would cost an image build to keep the shipped suite # # byte-identical. # # python3-tabulate 0.9.0 (meta-openembedded/meta-python, # python3-tabulate_0.9.0.bb, `inherit pypi python_setuptools_build_meta') -- THE # TABLE FORMATTER, on the wasm32-hwjs glibc image. Rows of data in, a laid-out # block of text out, in any of thirty-six documented styles. # # ============================================================================ # WHAT THE PACKAGE IS, AND THEREFORE WHAT THIS SUITE ASSERTS. # ============================================================================ # # tabulate has ONE public function and its entire product is A BLOCK OF TEXT. # There is no state, no stream and no side effect to observe -- so unlike a # library with an API surface to poke, the only honest assertion here is the # rendered block itself, character for character. This suite therefore asserts # WHOLE BLOCKS rather than properties of them, and it does so along two axes # that fail separately: # # 1. LAYOUT. Nine `tablefmt' styles over ONE fixture. Because the input is # held constant, the difference between any two of these blocks is the # difference between the two formats and nothing else. # # 2. ALIGNMENT. The same fixture in ONE format (`simple') with exactly one # keyword changed per case -- `numalign', `stralign', `floatfmt', # `missingval'. Each block's diff against `t_simple' is the whole of that # keyword's effect, which is what makes it an assertion about the keyword. # # A suite asserting only (1) would pass on a build that had lost every alignment # rule and padded to the left; one asserting only (2) would pass on a build that # rendered every style as `simple'. # # ============================================================================ # THE FIXTURE IS CHOSEN, NOT ARBITRARY. # ============================================================================ # # Three rows of a string, an int and a float: apple/3/1.5, banana/12/0.25, # cherry/100/12.125. The three floats carry ONE, TWO and THREE decimal places on # purpose. tabulate's default `numalign' is "decimal" -- it lines the decimal # POINTS up rather than the right edges -- and with three equal-width floats that # default is indistinguishable from plain right alignment. With these three it is # distinguishable, and `numalign-right' below is the control that proves it: it # is a DIFFERENT block from the default, which it could not be if the default # were right alignment. # # `plain-dec-cols' states the same fact as a number rather than leaving it # implicit in a block. A reader diffing two four-line blocks cannot see WHICH # property broke; that case says "the decimal point is at column 18 in all three # rows" in one line, and reds on its own terms. # # ============================================================================ # WHY THE BLOCKS ARE FLATTENED, AND WHY EACH ALSO CARRIES A DIGEST. # ============================================================================ # # A key=value line is ONE line and a table is several, so the probe appends the # token `' to every line of every block, the last one included. # # THAT IS LOAD-BEARING, NOT COSMETIC. `plain', `simple' and `rst' end each data # line at its last non-blank character, while `github', `grid', `pipe' and # `orgtbl' pad up to a closing `|'. The presence or absence of trailing spaces is # therefore part of what distinguishes these formats -- and a trailing space is # precisely what a console capture is entitled to eat. With `' appended, no # asserted space is ever the last character of anything. # # NOTHING OUTSIDE PRINTABLE ASCII IS COMPARED. Two formats need it: `tsv' # separates with TAB, and `fancy_grid' draws its rules with the Unicode # box-drawing set. A raw TAB would be re-spaced by whatever reads the line and a # raw box-drawing character would depend on the capture path's encoding -- the # assertion would then be about the console rather than about tabulate. The probe # renders TAB as `' and every non-ASCII codepoint as `', so every # value below is printable ASCII and a shell `=' is safe. # # EACH BLOCK IS ASSERTED TWICE: as the rendered text, and as a sha256 OF ITS TRUE # BYTES taken BEFORE the rendering. The rendered form is what makes a red # readable -- want and got print as text a person can diff by eye. The digest is # what makes the rendering unfalsifiable: any difference the flattening could # have hidden still moves the digest. Neither alone is enough, and they are # independent statements about the same block. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT `module-no-so' IS REALLY FOR. # ============================================================================ # # 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 therefore not be imported here -- not # "would be slow", could not be imported. tabulate is pure Python: the whole # library is a single `tabulate/__init__.py' beside a generated `version.py', and # the sdist contains no .c, no .pyx and no ext_modules. That is the entire reason # it ports. # # So `module-no-so' is a REGRESSION TRIPWIRE on a version bump, not boilerplate, # and it is anchored on the package's own dpkg file list as well as on a find -- # the two fail differently. A find catches a file the package does not declare; # the list catches a declared file that is not Python. # # ============================================================================ # THE OPTIONAL DEPENDENCY IS ABSENT, AND THAT IS ASSERTED RATHER THAN ASSUMED. # ============================================================================ # # tabulate does `import wcwidth' inside a try and sets # `WIDE_CHARS_MODE = wcwidth is not None'. With wcwidth present, the displayed # width of a string is `wcwidth.wcswidth(s)'; with it absent, it is `len(s)'. # wcwidth is NOT in this recipe's RDEPENDS and is not on this image, so the # expected arm is the absent one. # # THAT MATTERS TO EVERY OTHER CASE IN THIS FILE, because every column width # asserted below is a width computed by that arm. For the ASCII fixture used here # the two arms agree, so no block would move -- but the fact that they agree is a # property of the fixture, not a general one, and a later reader changing the # fixture to anything wide needs to know which arm is live. `wcwidth-absent' and # `wide-chars-mode-off' say so in the suite rather than in a comment. # # THOSE TWO CASES ARE A TRIPWIRE AND IT IS NOW ARMED. wcwidth was absent from the # image these expectations were measured on, which is why they are written to the # absent arm. python3-wcwidth landed on this layer under BRINTOS-1961 after that # measurement, so on any image carrying both packages these two cases RED -- by # design, announcing that the width arm moved from len() to wcwidth.wcswidth. # When it fires, flip the two to the present arm and RE-MEASURE; do not delete # them, and do not read the red as a tabulate defect. Every other case here is # expected to survive the flip because this fixture is printable ASCII, where the # two arms agree -- reasoning, not a measurement, so a block or digest moving # alongside them means the fixture assumption is what to re-examine. # # ============================================================================ # THE CONSOLE SCRIPT IS PART OF THE PACKAGE AND IS RUN, NOT JUST STAT'd. # ============================================================================ # # The recipe ships `/usr/bin/tabulate' from the `[project.scripts]' entry point # `tabulate = tabulate:_main'. It is asserted THREE ways, because they fail # differently: the file exists and is executable; `_main' exists as a name in the # module (a release that renamed it would leave a working library behind a broken # command); and the command is RUN over a fixture file, with its stdout compared # to the block the API produced in the same boot. # # THE CLI'S EXPECTATION IS THE API'S OWN OUTPUT, not a second copy of the # literal. `cli-matches-api' compares the command's stdout to `$(pv t_simple)' -- # the value the probe measured moments earlier on this same image. That is the # ticket's criterion exactly, and it is the case that would catch a CLI which had # drifted from the library it wraps. `cli-output' then compares the same stdout # to the hard-coded literal, so that a build in which BOTH had drifted the same # way cannot pass by agreeing with itself. # # The script's shebang is `#!/usr/bin/env python3', so it needs `/usr/bin/env' on # PATH resolution. `cli-env-present' asserts that separately: with env missing, # the two CLI cases above would red with an exec failure and nothing would say # why. # # ============================================================================ # EVERY ASSERTED VALUE WAS MEASURED, NOT READ OFF THE SOURCE. # ============================================================================ # # On x86-64 CPython 3.12.3, against THIS EXACT PROBE, three times, and against # TWO INDEPENDENT COPIES of the module: the sdist whose SRC_URI sha256 this # recipe pins # (0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c, verified by # digest before measuring) and the module unpacked from THIS build's own .deb. # The two runs were identical apart from `module_file', and the three repeats # byte-identical. The image ships python3.12 and the control is 3.12, so there is # no interpreter version gap to declare. # # What makes the control transferable is that all of it is pure Python over # content-determined text: column widths from len(), decimal alignment from # string positions, `floatfmt' from CPython's own float formatting. Nothing # asserted depends on pointer width, byte order or the platform string. # # THE ON-TARGET RESULT IS NOT YET MEASURED. This port's first run on wasm32-hwjs # is the batch verify; nothing in this file is a report of what happened there. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE OTHER TWENTY-SEVEN FORMATS. `formats' asserts the whole roster by name, # so one that was added or removed is caught; the nine the ticket names are # the ones rendered. Rendering all thirty-six would triple this file to assert # the same machinery through more codepaths. # - WIDE / CJK COLUMNS. Needs wcwidth, which is not on this image -- see above. # `wcwidth-absent' bounds it instead of a case pretending to measure it. # - THE ptest PACKAGE. The recipe does not `inherit ptest', so none is built. # Upstream's tests live in a top-level `test/' directory that the wheel does # not carry, so they are not on the image at all; `pkg-only-python' would # notice if a release started shipping them. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-tabulate # THE CANARY, which asserts nothing about python3-tabulate. # # 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 TB=$SITE/tabulate DI=$SITE/tabulate-0.9.0.dist-info CLI=/usr/bin/tabulate PKGLIST=/var/lib/dpkg/info/python3-tabulate.list D=$BST_DATA PROBE=$D/python3-tabulate-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-tabulate.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import tabulate'` further down is # the functional statement; these are the ones that say WHICH copy answered, so a # module that imported out of some other path is distinguishable from one that # imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$TB" # The whole of the library, named individually. There are only two files and that # is itself the fact: `__init__.py' IS tabulate -- every format, every alignment # rule and `_main' are in that one file -- and `version.py' is generated by # setuptools_scm at build time. A release that split the library would land here. check $P/module-init test -f "$TB/__init__.py" check $P/module-version test -f "$TB/version.py" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. 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' on a later ticket. check_eq $P/module-no-so "" \ "$(find "$TB" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # A find sees what is on disk; the dpkg list sees what the package CLAIMS. # # 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 UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a data file, or # upstream's `test/' tree arriving in a later release -- without having to # enumerate what it might be. # # 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. The scan # is scoped to $SITE deliberately, which is why it does not trip on the console # script under /usr/bin -- that file has its own cases below. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -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 ---- # What makes the installed module discoverable by importlib.metadata; a package # whose module landed but whose metadata did not would import fine here and be # invisible to any dependency resolver on the image -- and `dist-version' below, # which reads the metadata through importlib, is the case that would go red for # it. # # The licence sits at the TOP LEVEL of the dist-info, not under a `licenses/' # subdirectory as python3-colorama's does. That difference is asserted rather # than smoothed over, so a packaging change that moved it is visible here. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-license test -f "$DI/LICENSE" # The entry point that produces the console script, asserted in the metadata as # well as by the script's presence: these are two halves of one mechanism and a # build can get either wrong on its own. check $P/dist-info-entrypoints test -f "$DI/entry_points.txt" check_out $P/entrypoint-console "tabulate = tabulate:_main" cat "$DI/entry_points.txt" check_out $P/dist-info-top-level "tabulate" cat "$DI/top_level.txt" # THE VERSION THE RECIPE PINS, from the metadata rather than from the module. # Together with $P/version below -- which reads tabulate.__version__ -- this is # two independent statements of the same fact, and the two disagreeing would mean # a mixed install: a dist-info from one version beside a module from another. The # directory name carries it a third time, in $DI itself. check_out $P/dist-info-version "Version: 0.9.0" cat "$DI/METADATA" # --------------------------------------------------------- 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-tabulate 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. The # anchor is exact because a package named `python3-tabulate-' tomorrow # would otherwise be read as this one. sed -n '/^Package: python3-tabulate$/,/^$/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 dpkg RECORDED. One case per dependency, against the # extracted line, rather than one case asserting the whole list as a string: the # recipe's RDEPENDS order is not dpkg's to preserve and the separator spacing is # dpkg's own, so an exact-string case would be asserting a FORMAT rather than a # fact. Split, each red also names which dependency went missing. # # All five are sub-packages of the ported python3 recipe, which is why this port # waited on no other package. 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-html "python3-html" cat "$W/dep" check_out $P/pkg-depends-io "python3-io" cat "$W/dep" check_out $P/pkg-depends-math "python3-math" cat "$W/dep" check_out $P/pkg-depends-profile "python3-profile" cat "$W/dep" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If tabulate 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, and it names the one function the package is FOR. check $P/import python3 -c 'from tabulate import tabulate' # ------------------------------------------------------------- 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 `t_grid' distinct from # `t_fancy_grid' and `version' from `dist_version'. 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 formatter that had lost every layout, when what actually happened is that no # measurement was taken. The word `unscorable' is PARSED -- brintos_local_verify # re-measures a suite that prints it on a fresh guest rather than trusting a # shared boot's numbers (BRINTOS-1644) -- so it goes in this structural check, # not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE VERSION THE RECIPE PINS, read from the module. A mismatch here means the # recipe and what actually landed have gone out of step, and every expectation in # this file is then describing a different tabulate. check_eq $P/version "0.9.0" "$(pv version)" check_eq $P/dist-version "0.9.0" "$(pv dist_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 layout # cases below statements about the SAME tabulate. check_eq $P/module-file "$TB/__init__.py" "$(pv module_file)" # THE PUBLIC SURFACE. `_main' is here because it is not an implementation detail: # it is the name the shipped console script imports, so a release that renamed it # would leave a working library behind a broken command. check_eq $P/has-tabulate "yes" "$(pv has_tabulate)" check_eq $P/has-tabulate-formats "yes" "$(pv has_tabulate_formats)" check_eq $P/has-simple-separated-format "yes" "$(pv has_simple_separated_format)" check_eq $P/has-main "yes" "$(pv has__main)" # THE FORMAT ROSTER, WHOLE. The nine blocks below catch a layout that CHANGED; # only this catches one that was ADDED or REMOVED, which is the shape a version # bump takes. Sorted and comma-joined so a red prints the difference rather than # a boolean, and the count asserted beside it so a red that is hard to read as # text still says how many moved. check_eq $P/formats \ "asciidoc,double_grid,double_outline,fancy_grid,fancy_outline,github,grid,heavy_grid,heavy_outline,html,jira,latex,latex_booktabs,latex_longtable,latex_raw,mediawiki,mixed_grid,mixed_outline,moinmoin,orgtbl,outline,pipe,plain,presto,pretty,psql,rounded_grid,rounded_outline,rst,simple,simple_grid,simple_outline,textile,tsv,unsafehtml,youtrack" \ "$(pv formats)" check_eq $P/formats-count "36" "$(pv formats_count)" # THE OPTIONAL WIDE-CHARACTER DEPENDENCY, ABSENT, FROM BOTH SIDES OF THE SAME # try/except. See the header: every column width asserted below is a width # computed by the len() arm, and these two cases are what say that arm is the # live one on this image. check_eq $P/wcwidth-absent "no" "$(pv wcwidth_present)" check_eq $P/wide-chars-mode-off "no" "$(pv wide_chars_mode)" # --------------------------------------------------------- the nine layouts -- # One fixture, nine formats. Each is asserted as the rendered block AND as a # sha256 of its true bytes; the header says why neither alone is enough. The # line count is asserted too -- it is the cheapest red to read when a table has # grown or lost a rule, and it names that failure before the eye has to diff two # long strings. # `plain': columns separated by spaces, no rules at all. The baseline every other # format adds to, and the one whose data lines carry NO trailing padding. check_eq $P/plain "item qty priceapple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv t_plain)" check_eq $P/plain-lines "4" "$(pv t_plain_lines)" check_eq $P/plain-sha256 "9f7276c1651df78cc9a10be7b810efd2f50f27afde6dbbc460a1af4646e65a9b" "$(pv t_plain_sha256)" # `simple': `plain' plus one rule of dashes under the header. This is the default # format and the one the console script emits, so it is the block `cli-output' # below is compared against. check_eq $P/simple "item qty price------ ----- -------apple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv t_simple)" check_eq $P/simple-lines "5" "$(pv t_simple_lines)" check_eq $P/simple-sha256 "a5f396b58c0e5962f62b91cb77b2de7bc95ba8ffe71429c6abc141cf3c56e227" "$(pv t_simple_sha256)" # `github': pipe-delimited with a dashed header rule -- GitHub-flavoured # markdown. Note the cells are PADDED to the column width here, unlike `plain', # which is the trailing-space distinction the `' token exists to preserve. check_eq $P/github "| item | qty | price ||--------|-------|---------|| apple | 3 | 1.5 || banana | 12 | 0.25 || cherry | 100 | 12.125 |" "$(pv t_github)" check_eq $P/github-lines "5" "$(pv t_github_lines)" check_eq $P/github-sha256 "6123176bb2fb013509edb8ed5a234334971a3be8571e07f1c133f714a3b59ea2" "$(pv t_github_sha256)" # `grid': a rule between EVERY row, and `=' rather than `-' under the header. # Nine lines from three rows, which is what `grid-lines' states directly. check_eq $P/grid "+--------+-------+---------+| item | qty | price |+========+=======+=========+| apple | 3 | 1.5 |+--------+-------+---------+| banana | 12 | 0.25 |+--------+-------+---------+| cherry | 100 | 12.125 |+--------+-------+---------+" "$(pv t_grid)" check_eq $P/grid-lines "9" "$(pv t_grid_lines)" check_eq $P/grid-sha256 "091feb1627298b4a9cfbd6f9787213354d92d7c63511b6e9b6a752c12555d896" "$(pv t_grid_sha256)" # `fancy_grid': `grid' drawn with the Unicode box-drawing set. THE ONLY NON-ASCII # FORMAT ASSERTED HERE, and the reason the probe renders codepoints as # `' -- see the header. Every rule character is named individually by # this comparison, so a build whose encoding mangled one is caught by the # codepoint rather than by a box that merely looks wrong. check_eq $P/fancy-grid " item qty price apple 3 1.5 banana 12 0.25 cherry 100 12.125 " "$(pv t_fancy_grid)" check_eq $P/fancy-grid-lines "9" "$(pv t_fancy_grid_lines)" # THE DIGEST IS THE LOAD-BEARING HALF FOR THIS ONE. It is taken over the UTF-8 # encoding of the true block, so it asserts the ENCODING as well as the # codepoints -- which the `' rendering above deliberately cannot. check_eq $P/fancy-grid-sha256 "240fefce7a9ae39001dcc5eebbe503d08331eb4e5548ac190a68fd36e90b7f4c" "$(pv t_fancy_grid_sha256)" # `pipe': markdown with ALIGNMENT COLONS in the rule -- `:---' for the left # aligned string column, `---:' for the two right-aligned numeric ones. That rule # line is the only place in this suite where the alignment decision is visible as # syntax rather than as whitespace. check_eq $P/pipe "| item | qty | price ||:-------|------:|--------:|| apple | 3 | 1.5 || banana | 12 | 0.25 || cherry | 100 | 12.125 |" "$(pv t_pipe)" check_eq $P/pipe-lines "5" "$(pv t_pipe_lines)" check_eq $P/pipe-sha256 "b7bf309c2d98b2e7145fddc2d4cb37f462cabe68d3cbfd64c54b5766e6fe4d3b" "$(pv t_pipe_sha256)" # `orgtbl': Emacs org-mode. Identical to `github' except that the header rule # joins with `+' instead of `|', which is exactly the kind of one-character # difference a whole-block assertion catches and a "looks like a table" check # does not. check_eq $P/orgtbl "| item | qty | price ||--------+-------+---------|| apple | 3 | 1.5 || banana | 12 | 0.25 || cherry | 100 | 12.125 |" "$(pv t_orgtbl)" check_eq $P/orgtbl-lines "5" "$(pv t_orgtbl_lines)" check_eq $P/orgtbl-sha256 "5da0c214908cd67e71e0274906469d4b4face984211769b0b128885ccd81d0dd" "$(pv t_orgtbl_sha256)" # `rst': reStructuredText simple table -- a rule of `=' ABOVE the header as well # as below it and below the body, which is why this is seven lines from the same # three rows. check_eq $P/rst "====== ===== =======item qty price====== ===== =======apple 3 1.5banana 12 0.25cherry 100 12.125====== ===== =======" "$(pv t_rst)" check_eq $P/rst-lines "7" "$(pv t_rst_lines)" check_eq $P/rst-sha256 "f33f9943195911bdf9a446d3b1786f3d3897563221f99fab6bcf55484ef39f72" "$(pv t_rst_sha256)" # `tsv': TAB-separated. THE CELLS ARE STILL PADDED -- tabulate lays the columns # out and then joins with a TAB, rather than emitting bare values, which is the # surprising part of this format and the reason it is asserted rather than # assumed. The TABs are rendered `' for the reason in the header. check_eq $P/tsv "item qty priceapple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv t_tsv)" check_eq $P/tsv-lines "4" "$(pv t_tsv_lines)" check_eq $P/tsv-sha256 "6486e5a6be1c34f4b75c6c175c37c126c79e381095c5ef9a753828b91c994625" "$(pv t_tsv_sha256)" # ------------------------------------------------------- the alignment rules -- # Each block below is the SAME fixture in the SAME `simple' format as # $P/simple above, with exactly ONE keyword changed. Its diff against that block # is the whole of the keyword's effect. # DECIMAL ALIGNMENT IS THE DEFAULT, and this is the case that says so as a # number: the `.' of 1.5, 0.25 and 12.125 all land at column 18. Under plain # right alignment they could not, because the three floats have different numbers # of decimal places -- which is why the fixture was chosen that way. check_eq $P/plain-dec-cols "18,18,18" "$(pv plain_dec_cols)" # `numalign="left"': numbers flush left. Note the `item' column narrows too -- # the header `qty' is now wider than its column's content needs, so the layout # shifts as a whole rather than just within one column. check_eq $P/numalign-left "item qty price------ ----- -------apple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv a_numalign_left)" check_eq $P/numalign-left-sha256 "eea70cb4193983fc6a278a4c79351828e16fe7fa08473e10c4eeed93f53a9ff9" "$(pv a_numalign_left_sha256)" # `numalign="right"': flush right. THIS IS THE CONTROL FOR THE DEFAULT. It is a # DIFFERENT block from $P/simple -- `1.5' ends where `0.25' and `12.125' end, # rather than lining its decimal point up with theirs -- and that difference is # what proves the default is decimal alignment rather than right alignment. check_eq $P/numalign-right "item qty price------ ----- -------apple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv a_numalign_right)" check_eq $P/numalign-right-sha256 "4af4f52ba1aca9fa9d6e68b3abc2d327fedc0d17e8eab9d324f9e26076fe015e" "$(pv a_numalign_right_sha256)" # `stralign="right"': the string column flush right, the numeric columns # untouched. Asserting both string cases matters because `stralign' and # `numalign' are separate settings over separate column TYPES, and a build that # had conflated them would move the wrong column. check_eq $P/stralign-right " item qty price------ ----- ------- apple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv a_stralign_right)" check_eq $P/stralign-right-sha256 "9e6d1de234f74febde1cdd77130b3d290bc8f7d9d8f791df7d022a58d7edfbec" "$(pv a_stralign_right_sha256)" # `stralign="center"'. With a six-character column and a five-character longest # value the centring is visible only on the header, which is precisely the sort # of one-space fact a whole-block assertion holds and a spot check loses. check_eq $P/stralign-center " item qty price------ ----- -------apple 3 1.5banana 12 0.25cherry 100 12.125" "$(pv a_stralign_center)" check_eq $P/stralign-center-sha256 "4c4cc281aa803c3d9a8504a2cb2c4d0bca0670a25c8939465e828430f19c6188" "$(pv a_stralign_center_sha256)" # `floatfmt=".2f"': every float to two decimal places. 12.125 becomes 12.12, NOT # 12.13 -- CPython's format() rounds a tie to even, and asserting the value that # actually comes out rather than the one school arithmetic predicts is the whole # point of measuring instead of guessing. With all three floats now the same # width, the column is also right-aligned as a side effect. check_eq $P/floatfmt "item qty price------ ----- -------apple 3 1.50banana 12 0.25cherry 100 12.12" "$(pv a_floatfmt_2f)" check_eq $P/floatfmt-sha256 "130a655570bc9d9ef5b9140d344a4e1f9d80557b4265d6fe0cea20ee2ef53844" "$(pv a_floatfmt_2f_sha256)" # `missingval', on a fixture that HAS holes. A None in the data is a different # INPUT, not a different formatting of the same input, so it gets its own rows -- # two of them, with the hole in a different column each time, so a build that # substituted only in the numeric column or only in the last one is caught. # # The default is the EMPTY string, and the default block is asserted alongside: # without it, `missingval' could be passing on a build that rendered `n/a' # unconditionally. check_eq $P/missing-default "item qty price------ ----- -------apple 3banana 0.25" "$(pv a_missing_default)" check_eq $P/missing-default-sha256 "f35b640ee4b79f09064b8beeb9e1abc1d0c28d4af4abffbca41a5af4d7e91892" "$(pv a_missing_default_sha256)" check_eq $P/missingval "item qty price------ ----- -------apple 3 n/abanana n/a 0.25" "$(pv a_missingval)" check_eq $P/missingval-sha256 "00bccf3c2433f1bb9ae437fe19701e23a9dd62529eb7bb80b9ba484b43a50bee" "$(pv a_missingval_sha256)" # 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 done)" # ------------------------------------------------------- the console script -- # The package ships a command, not only a library. See the header for why it is # asserted three ways. check $P/cli-present test -f "$CLI" check $P/cli-executable test -x "$CLI" # The shebang is `#!/usr/bin/env python3'. With env missing the two cases below # would red with an exec failure and nothing would say why. check $P/cli-env-present test -x /usr/bin/env # The fixture: whitespace-separated columns, header row first, staged on tmpfs. # `-1' is the flag that makes the first row a header, which is what makes the # command's output comparable to a `headers=' call in the API. printf 'item qty price\napple 3 1.5\nbanana 12 0.25\ncherry 100 12.125\n' > "$W/fixture.txt" check $P/cli-fixture test -s "$W/fixture.txt" # The run. Its stdout is flattened the same way the probe flattens a block -- # `' appended to every line, nothing else touched -- so that the two are # comparable and so that no asserted trailing space is ever the last character. # `simple' is pure ASCII with no TABs, so no other rendering is needed. CLI_OUT="$("$CLI" -1 "$W/fixture.txt" 2>"$W/cli.err" | sed 's/$//' | tr -d '\n')" # THE TICKET'S OWN CRITERION: the command's stdout against the literal the API # produced in THIS boot, on THIS image, moments ago. A CLI that had drifted from # the library it wraps fails here. check_eq $P/cli-matches-api "$(pv t_simple)" "$CLI_OUT" # ...and against the hard-coded literal, so that a build in which the library AND # the command had drifted the same way cannot pass by agreeing with itself. check_eq $P/cli-output "item qty price------ ----- -------apple 3 1.5banana 12 0.25cherry 100 12.125" "$CLI_OUT" # The command wrote nothing to stderr. A run that had emitted a warning while # still producing the right table would pass both cases above; this is the one # that says the command was clean as well as correct. check_eq $P/cli-quiet "" "$(cat "$W/cli.err" 2>/dev/null)" rm -rf "$W"