. "$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 drives about twenty widgets into a StringIO, # # which is nothing beside the interpreter load. The three # # starts are `import', `import-submodules' and the probe -- # # this package ships no script, so there is no fourth. # # # # 1200 is 99-python3-texttable's and 99-python3-jstyleson'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-progress 1.6 (meta-openembedded/meta-python # recipes-devtools/python/python3-progress_1.6.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image -- PROGRESS BARS AND SPINNERS WITH # AN EXPLICIT next()/finish() SURFACE. Construct the widget, advance it, finish # it. Nothing is inferred and nothing is threaded: the caller decides when the # widget moves and where it writes. # # ============================================================================ # WHAT IS BEING ASSERTED IS A CARRIAGE-RETURN STREAM, NOT LINES. # ============================================================================ # # `Infinite.writeln' emits `\r' followed by the redrawn widget and NEVER a # newline -- redrawing in place is the whole mechanism. So a widget's output is # one run of characters whose CR positions are the content, and each stream is # asserted as # # -sha -- a 16-hex digest of the EXACT characters written, trailing # spaces and control bytes included, which is the case that says # nothing moved; and # -vis -- the same characters with `\r' shown as `~', `\n' as `' and # the two cursor escapes as `' / `', which is the # case a reviewer can check against the widget they expected. # # Neither is sufficient alone, and the visible form is not a convenience: a raw # CR in a shell literal is eaten by every editor and diff view that touches this # file, so `-vis' is the ONLY form in which the stream can be written down at # all, and `-sha' is the only one that pins the bytes. # # ============================================================================ # THE TTY GATE IS THE FACT THIS WHOLE SUITE RESTS ON. # ============================================================================ # # `writeln' writes only `if self.file and self.is_tty()', and `is_tty' calls # `self.file.isatty()' unless `check_tty' is False. A StringIO HAS isatty and # answers False -- so a widget handed one with the default `check_tty' writes # NOTHING AT ALL. Not a truncated widget: nothing. # # That is what makes this package assertable without a terminal, and it is the # single thing that would make every rendering case below red at once, so it is # MEASURED rather than assumed: `tty-gate-default-len' is 0 with the default, # `tty-gate-off-len' is non-zero with `check_tty=False', and # `tty-gate-default-index' is 2 -- the widget COUNTED, it just did not write. # `file-none' is the third silence, the one that needs no isatty at all because # `if self.file and ...' short-circuits, and `isatty-missing' is the documented # AttributeError a stream without isatty raises, whose message names the remedy. # # `hide_cursor' is False in every fixture but one, and that is not tidiness. # With it True, `Infinite.__del__' prints SHOW_CURSOR to the stream at an # unspecified time, so a stream read after the widget became garbage would differ # from one read before it. The `finish' fixture keeps it True and calls # `finish()', which clears `_hidden_cursor' so `__del__' adds nothing -- and that # clearing is asserted (`finish-cursor-cleared') because it is what makes the # stream stable. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, FOUR MODULES, NO OPTIONAL ANYTHING. # ============================================================================ # # 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. # # progress ships NO C: no .c, .pyx or .so in the sdist, no ext_modules, and # `packages=["progress"]' in setup.py. Its module-scope imports are `collections', # `datetime', `math', `sys' and `time' -- all stdlib, none of them a lib-dynload # .so in an ordinary CPython, which is the contrast with python3-texttable, whose # `import unicodedata' is a C extension everywhere but here. There is no optional # accelerator it prefers and no fallback path, so there is no `no-' case to # write: what a caller gets here is what the source says, unconditionally. # # `pkg-no-so' and `submodule-suffixes' are REGRESSION TRIPWIRES on a version # bump rather than statements about today, and `is-package' says it is a # DIRECTORY in site-packages rather than a single file -- the opposite shape from # python3-texttable and python3-jstyleson, and the reason `module-answered' names # an `__init__.py'. # # `pkg-depends' records what this port waits on, read back off the built .deb. # The recipe declares `RDEPENDS:${PN}:class-target += "python3-datetime # python3-math"', and both are sub-packages of the ported python3 recipe # (BRINTOS-1476) rather than third-party packages -- so nothing about this port # waits on another port. It is asserted as an EQUALITY rather than as a # substring, because a substring check would still pass if a release had added a # third-party dependency beside them, which on this image is the failure that # matters: the added package would have to be ported too. # # ============================================================================ # THE FIXTURE IS ONE ADVANCE SCHEDULE, AND `max' IS 8 ON PURPOSE. # ============================================================================ # # Every bar below is a `max' of 8 advanced four times, so the widgets differ only # in their own alphabet and suffix and a red names the widget rather than the # schedule. 8 is not arbitrary: it makes index/max a DYADIC RATIONAL at every # step, so `progress' is 0.125/0.25/0.375/0.5 and `percent' is # 12.5/25.0/37.5/50.0 as EXACT decimal text -- assertable as literals rather than # as a tolerance -- and `int(32 * progress)' lands on a whole character rather # than on a rounding. A `max' of 10 would have made `percent' 10.000000000000002 # and the case unwritable. # # THE INCREMENTAL BARS TAKE `max=5' INSTEAD, and that is the point of them. Their # whole difference from a plain `Bar' is the PARTIAL last character: `max=8' # fills a whole number of the 32 columns at every step, leaves `phase' zero # throughout, and would render PixelBar, ShadyBar and IncrementalBar as a plain # bar with a different fill -- passing while asserting nothing about the # machinery. `max=5' puts 32/5 = 6.4 columns in after one step, so the last # character is `phases[3]' and the phase arithmetic is actually exercised. # # THE SPINNERS ARE DRIVEN A FULL CYCLE PLUS ONE. They are `Infinite' -- no `max', # no percentage -- and `len(phases) + 1' advances put the WRAP back to phases[0] # in the stream. A spinner that ran off the end of its alphabet would pass a case # that stopped at the last phase. # # ============================================================================ # THE CLOCK IS REPLACED, NOT TOLERATED. # ============================================================================ # # `eta' and `avg' are derived from `monotonic()', which `progress/__init__.py' # binds at module scope -- so the probe rebinds `progress.monotonic' to a fixed # sequence and both become literals rather than a range. Each `next()' consumes # TWO ticks (one in `next', one inside `update_avg'), and the sequence is chosen # so the deltas are 1, 3, 2, 2 seconds and every `avg' a whole number. # # `clock-ticks-used' is 9 and is asserted: it is what says the probe's model of # how many times `next()' reads the clock still matches the implementation. A # release that read it once, or three times, would move every `avg' and `eta' # below at once, and this case is what names that as the cause. # # ============================================================================ # WHAT IS NOT COVERED. # ============================================================================ # # The recipe does NOT `inherit ptest', and upstream DOES ship `test_progress.py' # in the sdist -- so there is a suite that could be run and this image does not # run it. That is a choice about the meta-openembedded recipe rather than an # absence of material, and it is recorded here so the difference from a # ptest-carrying port is not left to be assumed. # # The COLOURED output is asserted only through its absence. `progress.colors' is # imported (`bar' does `from .colors import color') and every bar below draws # with `color=None', which `color()' passes through unchanged -- so no SGR escape # appears in any stream here, and `bar-vis' would show one if it did. Driving # each bar a second time through a foreground colour would double this file to # assert the same widget through a different eight-byte prefix. # # `MoonSpinner', `LineSpinner' and `PixelSpinner' are asserted by their PHASE # ALPHABET and their base class and are not driven. `Spinner' and `PieSpinner' # are, and the three undriven ones share `Spinner.update' unchanged -- so what a # drawing of them would add is their alphabet, which is already a case. # # ============================================================================ # WHERE THE ASSERTED VALUES COME FROM. # ============================================================================ # # Every literal below was produced by running data/python3-progress-probe.py on # x86-64 against the sdist the recipe's SRC_URI sha256 pins (c9c86e98...34cd, # verified by digest before measuring), and the payload cases were read back off # the built python3-progress_1.6-r0_wasm32.deb. The control interpreter is # CPython 3.10 and the image ships 3.12; that gap is stated here and in the test # plan rather than papered over. The on-target result is not yet measured -- the # batch verify is this port's first run on wasm32-hwjs. P=python3-progress # THE CANARY, which asserts nothing about python3-progress. # # 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/progress DI=$SITE/progress-1.6.dist-info PKGLIST=/var/lib/dpkg/info/python3-progress.list PROBE=$BST_DATA/python3-progress-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-progress.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import progress' 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 FILE. See the header: this is the opposite # shape from python3-texttable and python3-jstyleson, and a release that # collapsed it to one module would still import and still pass every behavioural # case below while the shape this image ships had changed. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/progress.py" ]; then echo present; else echo absent; fi)" # THE FOUR SUBMODULES, each on its own. `bar', `spinner' and `counter' are the # three the ticket names; `colors' is the one nobody imports directly and every # bar goes through -- `bar.py' does `from .colors import color' at module scope, # so a package that shipped without it would fail at `from progress.bar import # Bar' rather than at anything a caller wrote. for _m in bar colors counter spinner; do check $P/submodule-$_m test -f "$PKGDIR/$_m.py" done # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on # this image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- loudly, here, instead of at `import' later. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody predicted # -- a binary, a .pyd, a stray README -- without having to enumerate what it # might be. This package ships no script at all, so there is no executable to # exempt: anything outside site-packages is unexpected here by construction. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata. This package DOES declare a `__version__' -- setup.py # reads it back out of the package to set its own version -- so the version is # stated three independent ways: here, through importlib.metadata # (`dist-version'), and off the module itself (`version-attr'). `version-agrees' # is what says the three are one fact rather than three that happen to match. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.6" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import progress' has to match, and a wheel that renamed the package would # change it here first. check_out $P/dist-info-top-level "progress" cat "$DI/top_level.txt" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-progress is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take a -ptest, -dev or # -dbg stanza of the same family. sed -n '/^Package: python3-progress$/,/^$/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, as dpkg recorded them, asserted as an EQUALITY. See # the header: the recipe declares python3-datetime and python3-math, both # sub-packages of the ported python3 recipe, so this port waits on no other port # -- and that is the whole claim. A substring check would still pass if a release # had added a third-party dependency beside them. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends "Depends: python3-core, python3-datetime, python3-math" \ "$(sed -e 's/[[:space:]]*$//' "$W/dep")" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If progress 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 progress' # ...and the three submodules a caller actually reaches for, in one start. The # top-level `import progress' pulls in NONE of them -- `__init__.py' imports no # submodule -- so it would succeed on a package whose `bar.py' was missing. check $P/import-submodules python3 -c \ 'from progress.bar import Bar; from progress.spinner import Spinner; from progress.counter import Counter' # ---------------------------------------------------------------- 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 `bar_sha' distinguishable # from `pixel_bar_sha' and `pie_sha' from `pie_spinner_sha'. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ------------------------------------------------------------- the identity --- # The package 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 # rendering cases below statements about the SAME progress. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION, three ways. See the dist-info block. check_eq $P/version-attr "1.6" "$(pv version_attr)" check_eq $P/dist-version "1.6" "$(pv dist_version)" check_eq $P/version-agrees "yes" \ "$(if [ "$(pv version_attr)" = "$(pv dist_version)" ]; then echo yes; else echo no; fi)" # THE MODULES THAT WERE ACTUALLY LOADED, and what kind of file each came from. # The second is the tripwire the header describes: `py' and nothing else says no # part of this package arrived through a compiled extension, which on this image # could not have been loaded at all. check_eq $P/submodules "bar,colors,counter,spinner" "$(pv submodules)" check_eq $P/submodule-suffixes "py" "$(pv submodule_suffixes)" # THE TWO CURSOR ESCAPES, as module-scope constants. They are asserted here as # values and again in `finish-vis' coming out of a widget, which is what says the # constant and the behaviour are one fact. check_eq $P/hide-cursor-const "'\\x1b[?25l'" "$(pv hide_cursor_repr)" check_eq $P/show-cursor-const "'\\x1b[?25h'" "$(pv show_cursor_repr)" # ------------------------------------------------------------ the hierarchy --- # TWO ROOTS AND NOTHING ELSE. `Infinite' is for the widgets that do not know # where they are going; `Progress' is for the ones that do. Which root a widget # has is what decides whether `max', `remaining', `percent' and `eta' exist on it # at all -- so it is asserted per class rather than left to the import. The three # spinners this suite does not draw are covered here and by their alphabet. check_eq $P/mro-bar "Progress,Infinite" "$(pv base_Bar)" check_eq $P/mro-charging "Bar,Progress,Infinite" "$(pv base_ChargingBar)" check_eq $P/mro-squares "ChargingBar,Bar,Progress,Infinite" "$(pv base_FillingSquaresBar)" check_eq $P/mro-circles "ChargingBar,Bar,Progress,Infinite" "$(pv base_FillingCirclesBar)" check_eq $P/mro-incremental "Bar,Progress,Infinite" "$(pv base_IncrementalBar)" check_eq $P/mro-pixelbar "IncrementalBar,Bar,Progress,Infinite" "$(pv base_PixelBar)" check_eq $P/mro-shadybar "IncrementalBar,Bar,Progress,Infinite" "$(pv base_ShadyBar)" check_eq $P/mro-spinner "Infinite" "$(pv base_Spinner)" check_eq $P/mro-piespinner "Spinner,Infinite" "$(pv base_PieSpinner)" check_eq $P/mro-moonspinner "Spinner,Infinite" "$(pv base_MoonSpinner)" check_eq $P/mro-linespinner "Spinner,Infinite" "$(pv base_LineSpinner)" check_eq $P/mro-pixelspinner "Spinner,Infinite" "$(pv base_PixelSpinner)" check_eq $P/mro-counter "Infinite" "$(pv base_Counter)" check_eq $P/mro-countdown "Progress,Infinite" "$(pv base_Countdown)" check_eq $P/mro-stack "Progress,Infinite" "$(pv base_Stack)" check_eq $P/mro-pie "Stack,Progress,Infinite" "$(pv base_Pie)" # THE CLASS DEFAULTS EVERY RENDERING BELOW IS DRAWN AT. Asserted as values # because they are the shared cause: a release that changed `Bar.width' would # move every digest in this file at once, and these are the cases that name it. check_eq $P/bar-width "32" "$(pv bar_width)" check_eq $P/bar-suffix '%(index)d/%(max)d' "$(pv bar_suffix)" check_eq $P/bar-prefix "' |'" "$(pv bar_prefix_repr)" check_eq $P/bar-bar-suffix "'| '" "$(pv bar_bar_suffix_repr)" check_eq $P/bar-fill "#" "$(pv bar_fill)" check_eq $P/bar-empty-fill "' '" "$(pv bar_empty_fill_repr)" check_eq $P/charging-suffix '%(percent)d%%' "$(pv charging_suffix)" check_eq $P/charging-fill "█" "$(pv charging_fill)" check_eq $P/charging-empty "∙" "$(pv charging_empty_fill)" # The moving-average window and the default `max'. `sma_window' is the deque # bound `avg' is computed over, so it is the reason `clock-xput-len' below is 4 # rather than saturated; `default_max' of 100 is what a `Progress' built with no # `max' at all counts towards. check_eq $P/sma-window "10" "$(pv sma_window)" check_eq $P/default-max "100" "$(pv default_max)" # THE PHASE ALPHABETS, in order. They are the whole content of a spinner and the # last character of an incremental bar, so each is asserted as an ORDERED join -- # a set comparison would pass on an implementation that cycled them backwards -- # and the length beside it, because that is the modulus the cycling uses. check_eq $P/phases-spinner '-\|/' "$(pv phases_spinner)" check_eq $P/nphases-spinner "4" "$(pv nphases_spinner)" check_eq $P/phases-pie-spinner '◷◶◵◴' "$(pv phases_pie_spinner)" check_eq $P/nphases-pie-spinner "4" "$(pv nphases_pie_spinner)" check_eq $P/phases-moon-spinner '◑◒◐◓' "$(pv phases_moon_spinner)" check_eq $P/phases-line-spinner '⎺⎻⎼⎽⎼⎻' "$(pv phases_line_spinner)" check_eq $P/nphases-line-spinner "6" "$(pv nphases_line_spinner)" check_eq $P/phases-pixel-spinner '⣾⣷⣯⣟⡿⢿⣻⣽' "$(pv phases_pixel_spinner)" check_eq $P/phases-incremental ' ▏▎▍▌▋▊▉█' "$(pv phases_incremental)" check_eq $P/nphases-incremental "9" "$(pv nphases_incremental)" check_eq $P/phases-pixel-bar '⡀⡄⡆⡇⣇⣧⣷⣿' "$(pv phases_pixel_bar)" check_eq $P/nphases-pixel-bar "8" "$(pv nphases_pixel_bar)" check_eq $P/phases-shady-bar ' ░▒▓█' "$(pv phases_shady_bar)" check_eq $P/phases-stack ' ▁▂▃▄▅▆▇█' "$(pv phases_stack)" check_eq $P/phases-pie '○◔◑◕●' "$(pv phases_pie)" check_eq $P/nphases-pie "5" "$(pv nphases_pie)" # ------------------------------------------------------------- the tty gate --- # See the header. This is the fact every rendering case below rests on, and both # sides of it are measured rather than assumed. # # With the DEFAULT check_tty and a StringIO -- which has isatty and answers False # -- the widget writes nothing at all, while still counting. Those two facts # together are what say the gate is on `writeln' rather than on `next'. check_eq $P/tty-gate-isatty "no" "$(pv tty_gate_isatty)" check_eq $P/tty-gate-default-len "0" "$(pv tty_gate_default_len)" check_eq $P/tty-gate-default-index "2" "$(pv tty_gate_default_index)" # ...and with `check_tty=False' the same widget writes. 41 characters: the CR the # constructor's `writeln("")' emits, plus one 40-character redraw. check_eq $P/tty-gate-off-len "41" "$(pv tty_gate_off_len)" # `file=None' is the third silence, and the one that needs no isatty at all -- # `if self.file and ...' short-circuits before `is_tty' runs. It is the mode a # caller uses to disable a widget without changing the code around it. check_eq $P/file-none-index "1" "$(pv file_none_index)" # A stream with NO isatty and `check_tty' left True is the documented # AttributeError, and its message names the remedy. `str(self)' is in the message # and carries the object's address, so only the part after it is asserted. check_eq $P/isatty-missing-raises "yes" "$(pv isatty_missing_raised)" check_eq $P/isatty-missing-msg \ "has no attribute 'isatty'. Try setting check_tty=False." \ "$(pv isatty_missing_msg)" # ------------------------------------------------ the bars, THE DELIVERABLE --- # `Bar' at the default alphabet: 32 columns of `#', the `%(index)d/%(max)d' # suffix, four advances of a max of 8. The leading `~~' is not a typo -- the # first CR is the constructor's `writeln("")' and the second belongs to the first # redraw, and a widget that had stopped drawing at construction would lose one. check_eq $P/bar-sha "1fff665d94a7c15d" "$(pv bar_sha)" check_eq $P/bar-vis \ '~~Loading |#### | 1/8~Loading |######## | 2/8~Loading |############ | 3/8~Loading |################ | 4/8' \ "$(pv bar_vis)" check_eq $P/bar-index "4" "$(pv bar_index)" # `ChargingBar' is the same schedule through a different alphabet AND a different # suffix -- a PERCENTAGE, `%(percent)d%%'. It is the case that says `percent' is # rendered through `%d', so 12.5 comes out `12' and 37.5 comes out `37': a # TRUNCATION, not a rounding, and the fixture's half-integers are what make the # difference visible. check_eq $P/charging-sha "ca0d12c7c0422cc3" "$(pv charging_sha)" check_eq $P/charging-vis \ '~~Charging ████∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙ 12%~Charging ████████∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙ 25%~Charging ████████████∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙ 37%~Charging ████████████████∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙ 50%' \ "$(pv charging_vis)" # The two `ChargingBar' subclasses differ from it ONLY in `fill' and # `empty_fill', so these two cases are what say the alphabet is a class attribute # a caller can replace rather than something baked into `update'. check_eq $P/squares-sha "491606a79cc12af2" "$(pv squares_sha)" check_eq $P/squares-vis \ '~~Squares ▣▣▣▣▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢ 12%~Squares ▣▣▣▣▣▣▣▣▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢ 25%~Squares ▣▣▣▣▣▣▣▣▣▣▣▣▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢ 37%~Squares ▣▣▣▣▣▣▣▣▣▣▣▣▣▣▣▣▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢ 50%' \ "$(pv squares_vis)" check_eq $P/circles-sha "dbaa3a71e18754dd" "$(pv circles_sha)" check_eq $P/circles-vis \ '~~Circles ◉◉◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯ 12%~Circles ◉◉◉◉◉◉◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯ 25%~Circles ◉◉◉◉◉◉◉◉◉◉◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯ 37%~Circles ◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯ 50%' \ "$(pv circles_vis)" # ------------------------------------------- the incremental bars, max=5 ----- # See the header: `max=5' is what puts a PARTIAL character at the end of the bar, # which is the only thing distinguishing these three from a plain `Bar'. One # advance gives 32/5 = 6.4 columns: six full characters, then `phases[3]' -- # `⡇' for PixelBar, `▒' for ShadyBar, `▍' for IncrementalBar -- and the empty run # is shortened by the one character `current' occupies, which is the arithmetic # that keeps the bar 32 wide. check_eq $P/pixel-sha "7cc054c68f743a58" "$(pv pixel_sha)" check_eq $P/pixel-vis '~~Pixels |⣿⣿⣿⣿⣿⣿⡇ | 1/5' "$(pv pixel_vis)" check_eq $P/shady-sha "722e66a8bed5c300" "$(pv shady_sha)" check_eq $P/shady-vis '~~Shady |██████▒ | 1/5' "$(pv shady_vis)" check_eq $P/incr-sha "6abab860e5e7943c" "$(pv incr_sha)" check_eq $P/incr-vis '~~Incr |██████▍ | 1/5' "$(pv incr_vis)" # ------------------------------------------------------------- the spinners --- # Five advances through a four-phase alphabet, so the LAST phase in the stream is # the first phase again. Note the stream starts at `phases[1]': `update' is # called from `next', after the index moved, so a spinner's first drawn phase is # the second one -- which is behaviour, not an off-by-one, and asserting it is # what would catch a release that changed it. check_eq $P/spinner-sha "112ee2a632186b0a" "$(pv spinner_sha)" check_eq $P/spinner-vis '~~Spin \~Spin |~Spin /~Spin -~Spin \' "$(pv spinner_vis)" check_eq $P/spinner-wrapped "yes" "$(pv spinner_wrapped)" check_eq $P/pie-spinner-sha "4d7e00745f515850" "$(pv pie_spinner_sha)" check_eq $P/pie-spinner-vis '~~Pie ◶~Pie ◵~Pie ◴~Pie ◷~Pie ◶' "$(pv pie_spinner_vis)" # ------------------------------------------------------------- the counters --- # `Counter' is `Infinite' and counts UP with no ceiling; `Countdown' is # `Progress' and prints what is LEFT. The same four advances give 1,2,3,4 from # one and 7,6,5,4 from the other, which is the pair that says they are not the # same widget under two names. check_eq $P/counter-sha "230326370c274c22" "$(pv counter_sha)" check_eq $P/counter-vis '~~Count 1~Count 2~Count 3~Count 4' "$(pv counter_vis)" check_eq $P/countdown-sha "954946433afa2387" "$(pv countdown_sha)" check_eq $P/countdown-vis '~~Left 7~Left 6~Left 5~Left 4' "$(pv countdown_vis)" # `Stack' and `Pie' are ONE character wide -- the whole widget is a single glyph # picked out of the alphabet by `int(progress * nphases)'. They are worth # asserting beside each other because their alphabets are different LENGTHS: over # the same four steps a 9-phase Stack advances every time and a 5-phase Pie # repeats its second glyph, which is what says the index is scaled by the # alphabet rather than by the step. check_eq $P/stack-sha "6d24e46097186b89" "$(pv stack_sha)" check_eq $P/stack-vis '~~Stack ▁~Stack ▂~Stack ▃~Stack ▄' "$(pv stack_vis)" check_eq $P/pie-sha "e67d2dee4d37d8d2" "$(pv pie_sha)" check_eq $P/pie-vis '~~Pie ○~Pie ◔~Pie ◔~Pie ◑' "$(pv pie_vis)" # ------------------------------------------------------- finish(), the exit --- # THE TERMINATING SEQUENCE, end to end, and the ONE fixture that keeps # `hide_cursor' at its default True: HIDE_CURSOR at construction, then `finish()' # writing a newline and SHOW_CURSOR back. This is what a caller's terminal # actually depends on -- a widget that exited without the SHOW would leave the # cursor invisible in the user's shell. check_eq $P/finish-sha "49ca67145cd01bb9" "$(pv finish_sha)" check_eq $P/finish-vis \ '~~Done |#### | 1/8' \ "$(pv finish_vis)" # ...and `finish' CLEARS the flag, so `__del__' does not emit a second SHOW when # the widget is collected. See the header: that is what makes every stream in # this file stable whenever it is read. check_eq $P/finish-cursor-cleared "yes" "$(pv finish_hidden_cursor_cleared)" # The context manager is the same two calls with the widget deciding when. # `__enter__' calls `start()', which on a `Progress' DRAWS -- so the with-block # stream carries a leading `0/8' render a bare constructor does not, and # `__exit__' calls `finish()'. That leading render is the case: `Infinite.start' # is a no-op and `Progress.start' is not. check_eq $P/ctx-sha "f14392755afa9e35" "$(pv ctx_sha)" check_eq $P/ctx-vis \ '~~Ctx | | 0/8~Ctx |#### | 1/8' \ "$(pv ctx_vis)" # `iter' is the wrapper a caller actually reaches for, and on a `Progress' it # takes `max' from the ITERABLE'S OWN LENGTH -- so a sized iterable needs no # `max' at all, and the bar built with none reaches exactly 3/3. The yielded # values are asserted beside the stream because a generator that advanced the bar # but dropped an item would still draw correctly. check_eq $P/iter-values "a,b,c" "$(pv iter_values)" check_eq $P/iter-max "3" "$(pv iter_max)" check_eq $P/iter-sha "64cdd731a90a14bd" "$(pv iter_sha)" check_eq $P/iter-vis \ '~~Iter | | 0/3~Iter |########## | 1/3~Iter |##################### | 2/3~Iter |################################| 3/3' \ "$(pv iter_vis)" # `goto' is `next' by ABSOLUTE index rather than by delta, and it is the only way # to move a bar BACKWARDS -- which the second call does, 6 then 2. A caller # resuming an interrupted transfer uses it, and an implementation that clamped # `next' to non-negative deltas would red here and nowhere else. check_eq $P/goto-index "2" "$(pv goto_index)" check_eq $P/goto-sha "cfc7489cb1cb8d84" "$(pv goto_sha)" check_eq $P/goto-vis \ '~~Goto |######################## | 6/8~Goto |######## | 2/8' \ "$(pv goto_vis)" # ------------------------------------------------- the computed properties --- # Read DIRECTLY rather than through a rendering. `Bar''s own suffix shows only # `index' and `max', so a `progress' or `percent' that had drifted would not move # any digest above -- these are the cases that cover them, and see the header for # why `max=8' is what makes them exact decimal text. check_eq $P/prop-index "1,2,3,4" "$(pv prop_index)" check_eq $P/prop-remaining "7,6,5,4" "$(pv prop_remaining)" check_eq $P/prop-progress "0.125,0.25,0.375,0.5" "$(pv prop_progress)" check_eq $P/prop-percent "12.5,25.0,37.5,50.0" "$(pv prop_percent)" # THE TWO CLAMPS, which are the reason `progress' and `remaining' are properties # rather than arithmetic at the call site. Advanced 12 steps into a max of 8, the # index keeps counting -- it is the caller's number -- while `progress' saturates # and `remaining' floors, so the bar draws FULL rather than overflowing its 32 # columns. The suffix still reads `12/8', which is the honest thing to show. # # `overshoot-progress' is `1' and not `1.0': `min(1, index/max)' returns the INT # when the float side loses, so a caller formatting it with `%s' gets a different # string past the end than before it. That is upstream behaviour and it is # recorded rather than smoothed over. check_eq $P/overshoot-index "12" "$(pv overshoot_index)" check_eq $P/overshoot-progress "1" "$(pv overshoot_progress)" check_eq $P/overshoot-remaining "0" "$(pv overshoot_remaining)" check_eq $P/overshoot-sha "5028651e6fc752f0" "$(pv overshoot_sha)" check_eq $P/overshoot-vis '~~ |################################| 12/8' "$(pv overshoot_vis)" # `max=0' is the division this would otherwise be, and it is GUARDED: `progress' # returns 0 rather than raising ZeroDivisionError. A caller that computed a max # from an empty input gets an empty bar instead of a traceback, which is the # whole point of the guard. check_eq $P/zero-max-progress "0" "$(pv zero_max_progress)" check_eq $P/zero-max-percent "0" "$(pv zero_max_percent)" # `__getitem__' is what `message % self' and `suffix % self' go through, and its # one rule is that a key beginning with `_' answers None -- so a format string # cannot reach into the widget's private state. An unknown key answers None too # rather than raising, which is why a bad `%(typo)s' renders `None' instead of # killing the caller mid-progress. check_eq $P/getitem-public "4" "$(pv getitem_public)" check_eq $P/getitem-private "None" "$(pv getitem_private)" check_eq $P/getitem-unknown "None" "$(pv getitem_unknown)" # ------------------------------------------------ avg and eta, on a fixed clock --- # See the header: `progress.monotonic' is rebound to a fixed sequence, so these # are literals rather than a range. The deltas are 1, 3, 2, 2 seconds; `avg' is # the mean over the `sma_window' deque and `eta' is `ceil(avg * remaining)'. # # `clock-ticks-used' is the case that says the probe's model of the # implementation still holds: TWO clock reads per `next()', nine over the # fixture. A release that read it a different number of times would move every # value here at once, and this is what names that as the cause rather than # leaving four unexplained reds. check_eq $P/clock-avg "1.0,2.0,2.0,2.0" "$(pv clock_avg)" check_eq $P/clock-eta "7,12,10,8" "$(pv clock_eta)" check_eq $P/clock-ticks-used "9" "$(pv clock_ticks_used)" check_eq $P/clock-xput-len "4" "$(pv clock_xput_len)" # `elapsed' is read after the tick sequence is exhausted, so the standing clock # makes it the whole span of the fixture rather than a race with the host. The # two timedelta forms are what a caller renders with `%(elapsed_td)s' and # `%(eta_td)s', so their formatting is asserted as text. check_eq $P/clock-elapsed "8" "$(pv clock_elapsed)" check_eq $P/clock-elapsed-td "0:00:08" "$(pv clock_elapsed_td)" check_eq $P/clock-eta-td "0:00:08" "$(pv clock_eta_td)" # 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"