. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is 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 ~60 short strings and looks up # # seven CSS names, which is nothing beside the interpreter # # load. It matches 99-python3-elementpath's and # # 99-python3-xmltodict's 1200 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-ansicolors 1.1.8 (meta-openembedded # meta-python/recipes-devtools/python/python3-ansicolors_1.1.8.bb, `inherit pypi # setuptools3 ptest', PyPI sdist -- a .zip, not a .tar.gz, which is what # PYPI_PACKAGE_EXT in the recipe is for) on the wasm32-hwjs glibc image -- ANSI # COLOUR AND STYLE SEQUENCES WRAPPED AROUND STRINGS. `color(s, fg, bg, style)' # returns the string bracketed by one SGR sequence and a reset, and eighteen # module-level shortcuts (`red', `bold', ...) are partials over it. # # ============================================================================ # THE DISTRIBUTION IS `ansicolors' AND THE IMPORT IS `colors'. # ============================================================================ # # setup.py declares name='ansicolors' and packages=['colors'], so the dist-info # directory and the importable carry DIFFERENT names, and every case below has # to say which of the two it means: the payload cases are about $SITE/colors/, # the metadata cases about $SITE/ansicolors-1.1.8.dist-info/, and the dpkg cases # about python3-ansicolors. A port that got the mapping wrong would install # cleanly and fail only at `import'. # # ONE CONSEQUENCE IS A TRAP FOR ANYONE WRITING AGAINST THIS PACKAGE, and it is # why the probe never uses a star import. colors/__init__.py is # `from .colors import *', which binds the name `colors' to the SUBMODULE inside # the package namespace -- so a caller doing `from colors import *' ends up with # `colors' meaning colors.colors, not the package. Measured, not inferred: # `colors.__version__' raises AttributeError after a star import and resolves # after a plain one. `api-surface' below carries `colors' for that reason. # # ============================================================================ # WHAT THIS PORT ADDS THAT THE OTHER COLOUR LIBRARIES IN THIS ROUND DO NOT. # ============================================================================ # # The constant-table libraries beside it in BRINTOS-1948 give you NAMES for the # eight ANSI colours and the styles. This one gives you those too -- `red', # `bold' and the rest are here and are asserted one case each -- but its # `color()' also takes a 0..255 INTEGER and anything the CSS parser resolves, # which is what reaches the 256-colour and 24-bit escape forms a constant table # cannot express. So the input-form cases below are the port's actual subject: # # fg='red' -> ESC[31m base offset, 30 + COLORS.index # fg=33 -> ESC[38;5;33m the 256-colour form # fg='orange' -> ESC[38;2;...m 24-bit, resolved through css_colors # fg='#ff8800' -> ESC[38;2;...m 24-bit, 6-digit hex # fg='#f80' -> ESC[38;2;...m 24-bit, 3-digit hex, digits DOUBLED # fg='rgb(1,2,3)' -> ESC[38;2;1;2;3m # fg=(1,2,3) -> ESC[38;2;1;2;3m # # THREE DIFFERENT ESCAPE FAMILIES out of one entry point. A build that collapsed # any two of them into one could not pass all seven, which is why all seven are # separate cases rather than a spot check. # # ============================================================================ # NESTING DOES NOT RESTORE THE OUTER COLOUR. MEASURED, AND IT IS THE OPPOSITE # OF WHAT THIS TICKET EXPECTED. # ============================================================================ # # BRINTOS-1952's own description says a coloured span inside another "must # restore the outer colour rather than reset to plain". IT DOES NOT, and this is # a property of the library, not of this arm. color() closes every span with # ESC[0m, which is a FULL RESET and not a pop of a stack -- there is no stack -- # so: # # color('a' + red('b') + 'c', fg='green') # -> ESC[32m a ESC[31m b ESC[0m c ESC[0m # # and the `c' renders PLAIN. `nest-seq' asserts that byte sequence and # `nest-tail-plain' asserts the derived fact directly, so the suite states the # library's real behaviour rather than the behaviour the ticket predicted. A # release that grew a colour stack would red BOTH, and that would be a change # worth reading rather than a regression to fix. Recorded here, in the design # doc and in the test plan, because an expectation that survives into a document # unlabelled is indistinguishable from a measurement. # # ============================================================================ # THE UPSTREAM ptest IS NOT INSTALLED, AND ITS CASES ARE HERE INSTEAD. # ============================================================================ # # The recipe DOES inherit ptest, and BRINTOS-1952 asked for that suite to be run # as-is. It cannot be, and the reason is in the payload rather than in this arm: # test/test.py opens with `from pytest import raises' and `import pytest' at # module scope, and its cases are bare `test_*' functions rather than # unittest.TestCase methods -- so it does not run under `python3 -m unittest' # either. Running it needs python3-pytest, whose RDEPENDS name sixteen packages # of which python3-atomicwrites, python3-attrs, python3-pluggy, python3-py, # python3-pathlib2 and python3-wcwidth are not ported, plus # python3-unittest-automake-output. BRINTOS-1753 already rules both of the # recipe's declared -ptest RDEPENDS out of scope, and 99-python3-netaddr and # 99-python3-pyasn1 stand on the same ground. # # So `python3-ansicolors-ptest' is deliberately NOT on the image's list, and # nothing pulls those seven packages in. What is here instead is upstream's own # suite TRANSCRIBED: every value test/test.py asserts appears below as a case # against the same input, measured rather than copied out of the file. The # sections are marked `[upstream]' so the correspondence is checkable. The two # upstream cases NOT transcribed are test_unicode_strings_py2, which is # @skipif'd on Python 2 and this image is 3.12, and nothing else. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". # ============================================================================ # # 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. # # ansicolors ships no C: the sdist holds four .py files and nothing else that # builds, setup.py declares no ext_modules, and install_requires is EMPTY, so it # sits on nothing at all beyond the interpreter. `pkg-no-so' below is therefore a # REGRESSION TRIPWIRE on a version bump rather than a statement about today. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED. THE CONTROL AND THE TARGET AGREE ON PYTHON. # ============================================================================ # # On x86-64 (CPython 3.12.3), against THIS EXACT PROBE: # (a) the 1.1.8 sdist the recipe's SRC_URI sha256 pins # (99f94f5e3348a0bcd43c82e5fc4414013ccc19d70bd939ad71e0133ce9c372e0), # unpacked and put on PYTHONPATH -- not a pip copy and not upstream git; # (b) THE BYTES THIS RECIPE BUILDS -- python3-ansicolors_1.1.8-r0_wasm32.deb, # extracted, its site-packages on PYTHONPATH. # (a) and (b) agreed on every key. Recorded 2026-09-01; the ticket's design doc # carries the transcript. # # THE INTERPRETER GAP THAT USUALLY HAS TO BE STATED HERE IS ABSENT: the control # is CPython 3.12 and this image ships CPython 3.12, so unlike # 99-python3-elementpath there is no minor-version step between the two. What # has still not been performed anywhere is the wasm32-hwjs run itself; the batch # verify is the first. Nothing asserted below reads the clock, the environment, # hash randomisation, pointer width or byte order -- every value is a string # this module builds from constants. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE FULL 148-ENTRY CSS TABLE. `css-count' pins its size and four names are # resolved through it; enumerating the rest would assert a data file rather # than the port. # - show_colors.py, the sdist's demo script. It is not in `packages' and does # not install; there is nothing on the image to run. # - TERMINAL RENDERING. This library emits bytes and never reads a terminal: # no isatty check, no TERM lookup, no capability database. So the suite # asserts the BYTES and makes no claim about how the browser console draws # them -- the same escapes are asserted whether or not anything is attached. # - PYTHON 2 BEHAVIOUR (`_PY2', the `unicode' template branch, upstream's # test_unicode_strings_py2). Unreachable on a 3.12 image. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-ansicolors # THE CANARY, which asserts nothing about python3-ansicolors. # # 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/colors DI=$SITE/ansicolors-1.1.8.dist-info PKGLIST=/var/lib/dpkg/info/python3-ansicolors.list PROBE=$BST_DATA/python3-ansicolors-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-ansicolors.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import colors' 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 IT IS A DIRECTORY UNDER THE IMPORT NAME, NOT A FLAT MODULE AND NOT THE # DISTRIBUTION NAME. Two distinct wrong shapes, one case each: a release that # flattened to colors.py, and a packaging change that installed under the # distribution's own name. Either would leave much of this suite passing while # what this image ships had silently changed. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/colors.py" ]; then echo present; else echo absent; fi)" check_eq $P/no-ansicolors-dir "absent" \ "$(if [ -d "$SITE/ansicolors" ]; then echo present; else echo absent; fi)" # THE THREE SUBMODULES, each named. `import colors' succeeds while one is # missing only in the sense that it does not -- __init__ star-imports all three, # so a dropped file presents as an ImportError from inside the package rather # than as a missing attribute. One case each, so a red names which one went. check $P/pkg-module-colors test -f "$PKGDIR/colors.py" check $P/pkg-module-csscolors test -f "$PKGDIR/csscolors.py" check $P/pkg-module-version test -f "$PKGDIR/version.py" # 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 stray README, an installed show_colors.py -- without # having to enumerate what it might be. ansicolors declares NO package_data at # all, so unlike elementpath there is no data file to allow here: the expected # answer is that .py, .pyc and dist-info metadata are the whole payload. # # 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 ptest PAYLOAD IS NOT ON THIS IMAGE, asserted rather than left implied. See # the header: `python3-ansicolors-ptest' is deliberately off the roster because # running it needs python3-pytest and six unported packages behind it. If a # later change installed it, the ptest directory would appear here and the # reader should be sent to this comment rather than left to wonder why the # transcribed cases below duplicate a suite that is now present. check_eq $P/no-ptest-payload "absent" \ "$(if [ -d /usr/lib/python3-ansicolors/ptest ]; then echo present; else echo absent; fi)" # ----------------------------------------------------------- the dist-info --- # The installed metadata, which is the second, independent statement of the # version -- `$P/dist-version' below reads it through importlib.metadata, and the # directory name carries it a third time in $DI itself. A package whose modules # landed and whose metadata did not would import fine and serve every behavioural # case while reporting no installed version at all. 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.1.8" cat "$DI/METADATA" check_out $P/dist-info-name "Name: ansicolors" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs, and it is the # file that states the ansicolors/colors split in the packaging's own words. check_out $P/dist-info-top-level "colors" 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-ansicolors is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -ptest or -dev # stanza of the same family. sed -n '/^Package: python3-ansicolors$/,/^$/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 DEPENDENCY LINE, WHICH IS ONE PACKAGE LONG. The recipe declares no # RDEPENDS for ${PN} at all -- install_requires is empty and there is no C -- so # what dpkg recorded is the single dependency the packaging derived, # python3-core. Asserted as an EXACT line rather than as a substring: the # interesting failure here is not a dependency going missing but one appearing, # which is what a version that grew a third-party requirement would look like # and what a substring case would pass straight over. 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-exact "Depends: python3-core" \ "$(cat "$W/dep" 2>/dev/null)" # ...AND dpkg DOES NOT KNOW THE ptest PACKAGE. The payload case above says the # ptest FILES are not on the image; this says the PACKAGE was never installed, # which is the statement that python3-ansicolors-ptest is off the roster rather # than installed-and-empty. See the header for why it is off it. check_eq $P/no-ptest-package "absent" \ "$(if grep -q '^Package: python3-ansicolors-ptest$' /var/lib/dpkg/status 2>/dev/null; then echo present; else echo absent; fi)" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If colors 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 colors' # ...and the two submodules by name, which separates "csscolors is absent" from # "csscolors is present and wrong". The CSS table is what every 24-bit case # below depends on and it is the half a packaging change is likeliest to drop. check $P/import-submodules python3 -c 'import colors.colors, colors.csscolors' # ...and the headline callables by name, the import-level statement of the # surface. The behavioural halves are asserted further down. check $P/import-api python3 -c 'from colors import color, red, bold, strip_color, ansilen, parse_rgb' # ---------------------------------------------------------------- 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 `version' distinguishable # from `dist_version', and `fn_red' from `fn_red_matches_color'. 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 # EVERY KEY THE PROBE TRIED WAS PRODUCED. The probe wraps each measurement and # emits RAISED:: in place of a value rather than dying, so a single # broken attribute would otherwise show up only as one odd-looking case among # ninety. This names them as a set. check_eq $P/probe-no-raised "" "$(pv probe_failed_keys)" # ------------------------------------------------------------- the identity --- # THE VERSION THE RECIPE PINS, twice and independently: the package's own # `__version__' out of colors/version.py, and importlib.metadata over the # installed dist-info. The two disagreeing means a mixed install -- a dist-info # from one version beside a package from another. check_eq $P/version "1.1.8" "$(pv version)" check_eq $P/dist-version "1.1.8" "$(pv dist_version)" # THE TWO NAMES, from the interpreter's side. See the header: the distribution # is `ansicolors' and the importable is `colors', and these are the cases that # state the mapping rather than leaving it to the paths above. check_eq $P/dist-name "ansicolors" "$(pv dist_name)" check_eq $P/import-name "colors" "$(pv import_name)" # The package answered out of the files 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 colors. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/submodule-colors-answered "$PKGDIR/colors.py" "$(pv submodule_colors_file)" check_eq $P/submodule-csscolors-answered "$PKGDIR/csscolors.py" "$(pv submodule_csscolors_file)" # The interpreter this image ships, recorded beside the results rather than # assumed. The control measurement was CPython 3.12 (see the header); this case # is what tells a later reader whether the two were still the same minor version # when the numbers below were taken. check_eq $P/py-version "3.12" "$(pv py_version)" # THE PUBLIC SURFACE, WHOLE -- AND IT IS dir(), NOT __all__. ansicolors declares # no __all__, so this is what the star import in __init__.py happens to bind, # which is why `re', `sys', `partial', `string_types' and the two __future__ # flags stand in it beside the API. Asserted AS MEASURED and not as it ought to # be: a release that added __all__ would red here, and that is a change to read # rather than a regression. `colors' in this list is the submodule shadow the # header describes. check_eq $P/api-surface \ "COLORS,STYLES,absolute_import,ansilen,black,blink,blink2,blue,bold,color,colors,concealed,crossed,css_colors,csscolors,cyan,faint,green,is_string,italic,magenta,negative,none,parse_rgb,partial,print_function,re,red,string_types,strip_color,sys,underline,version,white,yellow" \ "$(pv api_surface)" # The two name tables, in ORDER, because the order IS the encoding: a colour's # escape code is 30 plus its index in COLORS and a style's is its index in # STYLES. A reordered tuple would leave every name present and every code wrong. check_eq $P/colors-tuple "black,red,green,yellow,blue,magenta,cyan,white" \ "$(pv colors_tuple)" check_eq $P/styles-tuple "none,bold,faint,italic,underline,blink,blink2,negative,concealed,crossed" \ "$(pv styles_tuple)" # The CSS table's size. Four of its names are resolved behaviourally below; this # pins the rest as payload without enumerating a data file. check_eq $P/css-count "148" "$(pv css_count)" # `red' and friends are functools.partial OBJECTS. A `def'-based rewrite upstream # would change what a caller can introspect and would red here first. check_eq $P/partial-type "partial" "$(pv partial_type)" # ------------------------------------------------------- the named functions -- # ONE CASE PER ANSI COLOUR, through the shortcut rather than through color(). # Each shortcut is a separately-bound partial, so a single mis-bound one is what # this catches -- and the codes ascend 30..37 in COLORS order, which is the # encoding the tuple case above only stated. check_eq $P/fn-black "ESC[30mXESC[0m" "$(pv fn_black)" check_eq $P/fn-red "ESC[31mXESC[0m" "$(pv fn_red)" check_eq $P/fn-green "ESC[32mXESC[0m" "$(pv fn_green)" check_eq $P/fn-yellow "ESC[33mXESC[0m" "$(pv fn_yellow)" check_eq $P/fn-blue "ESC[34mXESC[0m" "$(pv fn_blue)" check_eq $P/fn-magenta "ESC[35mXESC[0m" "$(pv fn_magenta)" check_eq $P/fn-cyan "ESC[36mXESC[0m" "$(pv fn_cyan)" check_eq $P/fn-white "ESC[37mXESC[0m" "$(pv fn_white)" # ONE CASE PER STYLE, same reasoning, codes 0..9 in STYLES order. # # `none' IS NOT A NO-OP AND THAT IS WHY IT IS HERE. It is style index 0, so it # emits ESC[0m as the OPENING code and ESC[0m again to close -- the string is # wrapped in two resets and renders plain. A `if style'-style truthiness bug # would drop it and leave the other nine working, which is exactly the shape a # spot check misses. check_eq $P/st-none "ESC[0mXESC[0m" "$(pv st_none)" check_eq $P/st-bold "ESC[1mXESC[0m" "$(pv st_bold)" check_eq $P/st-faint "ESC[2mXESC[0m" "$(pv st_faint)" check_eq $P/st-italic "ESC[3mXESC[0m" "$(pv st_italic)" check_eq $P/st-underline "ESC[4mXESC[0m" "$(pv st_underline)" check_eq $P/st-blink "ESC[5mXESC[0m" "$(pv st_blink)" check_eq $P/st-blink2 "ESC[6mXESC[0m" "$(pv st_blink2)" check_eq $P/st-negative "ESC[7mXESC[0m" "$(pv st_negative)" check_eq $P/st-concealed "ESC[8mXESC[0m" "$(pv st_concealed)" check_eq $P/st-crossed "ESC[9mXESC[0m" "$(pv st_crossed)" # [upstream] test_partial_functions -- the shortcut and the general call agree. # This is the statement that the partials are bound to THIS color() with THESE # arguments rather than merely producing a plausible sequence of their own. check_eq $P/fn-red-matches-color "yes" "$(pv fn_red_matches_color)" # ---------------------------------------------------- the color() input forms - # THE PORT'S ACTUAL SUBJECT. Three escape families out of one entry point -- # see the table in the header. Seven separate cases, not a spot check, because a # build that collapsed any two families into one could still pass a sample. # # [upstream] test_redcolor check_eq $P/form-ansi-name "ESC[31mXESC[0m" "$(pv form_ansi_name)" # [upstream] test_integer_color -- the 256-colour form, base+8 then `5;n'. check_eq $P/form-int256 "ESC[38;5;33mXESC[0m" "$(pv form_int256)" # [upstream] test_css_color -- a CSS NAME, resolved through the 148-entry table # to a triple and emitted as 24-bit. Note this is NOT the `orange' of COLORS: # there is no such ANSI colour, which is why it reaches the parser at all. check_eq $P/form-css-name "ESC[38;2;255;165;0mXESC[0m" "$(pv form_css_name)" check_eq $P/form-hex6 "ESC[38;2;255;136;0mXESC[0m" "$(pv form_hex6)" check_eq $P/form-hex3 "ESC[38;2;255;136;0mXESC[0m" "$(pv form_hex3)" check_eq $P/form-rgbfn "ESC[38;2;1;2;3mXESC[0m" "$(pv form_rgbfn)" # [upstream] test_tuple_color -- and a list, which the same isinstance branch # accepts and which no upstream case covers. check_eq $P/form-tuple "ESC[38;2;1;2;3mXESC[0m" "$(pv form_tuple)" check_eq $P/form-list "ESC[38;2;1;2;3mXESC[0m" "$(pv form_list)" # THE 3-DIGIT HEX EXPANDS BY DOUBLING EACH DIGIT, not by padding. `#f80' is # `#ff8800'; a parser that read it as a truncated 6-digit value would produce # (15, 8, 0) and would pass no other case. Asserted as an equality between the # two forms rather than only as two separate sequences, so the RELATION is what # reds. check_eq $P/hex3-equals-hex6 "yes" "$(pv hex3_equals_hex6)" # BACKGROUND IS THE SAME LADDER SHIFTED BY TEN -- base 40 rather than 30, and # 48;2 rather than 38;2. One ANSI name and one CSS name is enough to say the # base is applied rather than hard-coded. # [upstream] test_background_color check_eq $P/bg-ansi-name "ESC[41mXESC[0m" "$(pv bg_ansi_name)" check_eq $P/bg-css-name "ESC[48;2;128;0;128mXESC[0m" "$(pv bg_css_name)" # `default' IS THE ONE SPEC THAT IS NOT LOOKED UP: base+9, the ANSI "revert this # channel" code, matched before COLORS and before the CSS parser. # [upstream] test_default_color check_eq $P/fg-default "ESC[39mXESC[0m" "$(pv fg_default)" check_eq $P/bg-default "ESC[49mXESC[0m" "$(pv bg_default)" check_eq $P/fgbg-default "ESC[39;49mXESC[0m" "$(pv fgbg_default)" # ORDER INSIDE THE SEQUENCE IS PART OF THE DELIVERABLE: fg, then bg, then style, # semicolon-joined into ONE escape rather than emitted as three. Both mixed # orders are asserted because one sample cannot tell a stable order from an # accident. # [upstream] test_doc_example (second half), test_mixed_color check_eq $P/combo-fg-bg-style "ESC[31;43;4mXESC[0m" "$(pv combo_fg_bg_style)" check_eq $P/mixed-fg-bg \ "ESC[38;2;255;192;203;48;2;128;128;128mPINK/GRAYESC[0m" "$(pv mixed_fg_bg)" check_eq $P/mixed-bg-fg \ "ESC[38;2;128;128;128;48;2;255;192;203mGRAY/PINKESC[0m" "$(pv mixed_bg_fg)" # Styles combine on `+' and stack in the order written. check_eq $P/style-multi "ESC[1;4mXESC[0m" "$(pv style_multi)" # [upstream] test_nocolors -- NO SPEC RETURNS THE STRING UNTOUCHED. Not an empty # escape, not a bare reset: the `if codes' branch returns `s' itself. A build # that always wrapped would leave every uncoloured string carrying a stray # ESC[m, which nothing else here would notice. check_eq $P/no-spec "X" "$(pv no_spec)" check_eq $P/no-spec-identity "yes" "$(pv no_spec_identity)" # ------------------------------------------------------------- the nesting --- # MEASURED, AND THE OPPOSITE OF WHAT THIS TICKET EXPECTED -- see the header. # color() closes with ESC[0m, a FULL RESET rather than a pop, so text after an # inner span renders PLAIN and not in the outer colour. The byte sequence and # the derived fact are separate cases: the first says exactly what is emitted, # the second says what it means, and a release that grew a colour stack would # red both. check_eq $P/nest-seq "ESC[32maESC[31mbESC[0mcESC[0m" "$(pv nest_seq)" check_eq $P/nest-tail-plain "no" "$(pv nest_tail_restores_outer)" # -------------------------------------------------------- strip and ansilen --- # [upstream] test_remove_color check_eq $P/strip-simple "RED" "$(pv strip_simple)" check_eq $P/strip-multi "text" "$(pv strip_multi)" # [upstream] test_remove_color_extra -- the three sequences upstream names as # seen in the wild and NOT produced by this module: EL (erase to end of line), # the terse reset, and a two-channel revert. This is the half of strip_color() # that is about OTHER colourisers' output, so nothing else in the suite reaches # it: every string the cases above build comes out of color() itself. check_eq $P/strip-el "something" "$(pv strip_el)" check_eq $P/strip-terse "something" "$(pv strip_terse)" check_eq $P/strip-odd "something" "$(pv strip_odd)" # [upstream] test_ansilen -- the visible length, which is what a caller aligning # columns needs and the reason strip_color is public at all. Four inputs, one # answer: plain text, text carrying sequences this module never emits, a # shortcut's output, and a three-spec call. check_eq $P/ansilen-plain "4" "$(pv ansilen_plain)" check_eq $P/ansilen-el "4" "$(pv ansilen_el)" check_eq $P/ansilen-red "4" "$(pv ansilen_red)" check_eq $P/ansilen-multi "4" "$(pv ansilen_multi)" # --------------------------------------------------------------- parse_rgb --- # [upstream] test_parse_rgb -- the CSS resolver on its own, beneath color(). The # same colour reached three ways must be one triple, and `rebeccapurple' does it # a fourth way through the 3-digit hex, so the doubling expansion and the name # table are asserted to agree with each other rather than only with themselves. check_eq $P/prgb-name "(255, 218, 185)" "$(pv prgb_name)" check_eq $P/prgb-hex6 "(255, 218, 185)" "$(pv prgb_hex6)" check_eq $P/prgb-rgbfn "(255, 218, 185)" "$(pv prgb_rgbfn)" check_eq $P/prgb-rebecca-name "(102, 51, 153)" "$(pv prgb_rebecca_name)" check_eq $P/prgb-rebecca-hex3 "(102, 51, 153)" "$(pv prgb_rebecca_hex3)" check_eq $P/prgb-rebecca-rgbfn "(102, 51, 153)" "$(pv prgb_rebecca_rgbfn)" # Case and internal whitespace are normalised before lookup. Not an upstream # case; it is here because a caller passing a colour straight out of a config # file depends on it and nothing else would say so. check_eq $P/prgb-spaced-upper "(255, 218, 185)" "$(pv prgb_spaced_upper)" # ----------------------------------------------------------- the negatives --- # [upstream] test_error_on_bad_color_string, _int, _notint, _on_bad_style, # _setting_style_color -- every rejection upstream asserts, each a DIFFERENT # path into the same ValueError: an unknown name falls through the CSS parser, # an out-of-range int misses the 0..255 window and reaches the parser as a # non-string, a float misses the isinstance(int) test the same way, and a bad # style name is refused by color() itself before any colour code is computed. # # BOTH HALVES OF EACH, `raised' and `type'. `raised' alone cannot tell a # ValueError from a TypeError thrown by a signature that changed under us, and # a library that started refusing bad input with the wrong exception class # would break every caller's `except ValueError' while passing a raised-only # case. check_eq $P/err-bad-color-name-raised "yes" "$(pv err_bad_color_name_raised)" check_eq $P/err-bad-color-name-type "ValueError" "$(pv err_bad_color_name_type)" check_eq $P/err-bad-color-int-raised "yes" "$(pv err_bad_color_int_raised)" check_eq $P/err-bad-color-int-type "ValueError" "$(pv err_bad_color_int_type)" check_eq $P/err-bad-color-float-raised "yes" "$(pv err_bad_color_float_raised)" check_eq $P/err-bad-color-float-type "ValueError" "$(pv err_bad_color_float_type)" check_eq $P/err-bad-bg-name-raised "yes" "$(pv err_bad_bg_name_raised)" check_eq $P/err-bad-bg-name-type "ValueError" "$(pv err_bad_bg_name_type)" check_eq $P/err-bad-style-raised "yes" "$(pv err_bad_style_raised)" check_eq $P/err-bad-style-type "ValueError" "$(pv err_bad_style_type)" # parse_rgb refuses a non-string with a ValueError of its own rather than # letting a TypeError escape from re.match. Not an upstream case. check_eq $P/err-parse-rgb-nonstring-raised "yes" "$(pv err_parse_rgb_nonstring_raised)" check_eq $P/err-parse-rgb-nonstring-type "ValueError" "$(pv err_parse_rgb_nonstring_type)" # ------------------------------------------------------------- the unicode --- # [upstream] test_unicode_strings -- non-ASCII text passes through unchanged and # ansilen counts CODE POINTS, which is what a caller aligning columns needs. The # em dash and the u-umlaut are upstream's own fixture; 17 is the code-point # length of `This-is uber good' with those two characters in it. # # Upstream's test_unicode_strings_py2 is NOT transcribed: it is @skipif'd on # Python 2 and this image is 3.12. check_eq $P/uni-seq "ESC[31mThis—is über goodESC[0m" "$(pv uni_seq)" check_eq $P/uni-strip "This—is über good" "$(pv uni_strip)" check_eq $P/uni-ansilen "17" "$(pv uni_ansilen)" # ---------------------------------------------------- caller-built partials --- # [upstream] test_custom_partial -- the package re-exports functools.partial # itself (it is in `api-surface' above), and upstream's documented pattern is to # build your own shortcut with it. Four spellings of one colour+style must # produce one string, so this is the case that says the shortcuts are a PATTERN # a caller can extend rather than eighteen hard-coded specials. check_eq $P/custom-partial-agrees "yes" "$(pv custom_partial_agrees)" check_eq $P/custom-partial-seq "ESC[31;1;4mvery importantESC[0m" \ "$(pv custom_partial_seq)" # --------------------------------------------------- the documented examples -- # [upstream] test_doc_example -- the two strings the README puts in front of a # reader. They assert nothing the cases above do not, and they are here because # a README that stopped being true is a defect a user meets before any test. check_eq $P/doc-blue "ESC[34mmy stringESC[0m" "$(pv doc_blue)" check_eq $P/doc-underline "ESC[31;43;4msome textESC[0m" "$(pv doc_underline)" # 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"