. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is the whole cost. Everything # # below either reads the filesystem/dpkg with shell builtins # # or greps the single probe run's output; the probe itself # # does forty-one dictionary lookups and string formats over a # # one-character string, 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-termcolor 2.4.0 (meta-openembedded # meta-python/recipes-devtools/python/python3-termcolor_2.4.0.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc image -- # ANSI COLOUR FORMATTING FOR TERMINAL OUTPUT. `colored(text, color, on_color, # attrs)' returns the text wrapped in SGR escape sequences; `cprint' prints what # `colored' would have returned. # # ============================================================================ # THE ONE THING TO KNOW BEFORE BELIEVING ANY CASE HERE: colored() IS CONDITIONAL. # ============================================================================ # # `colored' is NOT an unconditional wrapper. It calls `_can_do_colour()' first # and RETURNS THE TEXT UNWRAPPED when stdout is not a tty. The probe's stdout is # a FILE -- bst_probe redirects it -- so a plain `colored("X", "red")' in this # suite would return `X', with no escape in it at all, and every colour case # would PASS while asserting nothing whatsoever about colour. # # The probe therefore turns colour on explicitly, through the ENVIRONMENT # (`FORCE_COLOR') and not through the `force_color=' keyword. The keyword # short-circuits `_can_do_colour' before it reads the environment, so the # suppression half of this port -- `ANSI_COLORS_DISABLED' and `NO_COLOR', which # is what the ticket is about -- could not be expressed against it. With # FORCE_COLOR as the baseline, those two are asserted to BEAT an active request # for colour, which is the no-color.org precedence and the only form in which # those cases say anything. # # ============================================================================ # ESC IS RENDERED AS THE THREE LETTERS `ESC', NEVER ASSERTED RAW. # ============================================================================ # # What this package produces IS control bytes, and the probe's stdout is read # back through the guest console into the verify capture. A raw 0x1b there would # be interpreted by whatever renders that capture -- the assertion would move the # cursor instead of being compared -- and would corrupt the capture for every # OTHER suite in the same boot. So the probe replaces `\033' with `ESC' on the way # out and every expectation below is written in that rendering. `ESC[31mXESC[0m' # means the five bytes ESC [ 3 1 m, then X, then ESC [ 0 m. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND FOUR FILES OF IT. # ============================================================================ # # 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. # # termcolor ships NO C: the sdist is `__init__.py', `termcolor.py', `_types.py', # `__main__.py' and a `py.typed' marker, with no ext_modules and no runtime # dependency outside the standard library. `pkg-no-so' below is therefore a # REGRESSION TRIPWIRE on a version bump rather than a statement about today. # # ============================================================================ # ONE RUNTIME DEPENDENCY, AND IT IS COMPLETE. # ============================================================================ # # dpkg records `Depends: python3-core' and nothing else. That is the whole of it # and it was checked rather than assumed: the module imports `io', `os', `sys', # `warnings' and `typing', and all five .py files of those live in python3-core's # own .deb on this build. So this port waits on nothing -- which is what the # ticket says, though for a narrower reason than it gives: `python3-toml-native' # and `python3-hatch-vcs-native' are the recipe's BUILD-time DEPENDS (the # hatchling backend and its vcs version source), and neither is a runtime # dependency or on the image at all. # # ============================================================================ # THERE IS NO `termcolor.__version__'. # ============================================================================ # # pyproject.toml declares `dynamic = ["version"]' with `version.source = "vcs"', # so the version exists ONLY in the metadata hatchling writes at build time -- # the module carries no `__version__' attribute for a case to read. That makes # `dist-version' the single statement of the version there is, and it is the one # a version-source misconfiguration in the recipe gets wrong: a build with no # git tree to read would ship `0.1.dev1' here while every behavioural case below # passed. `no-dunder-version' asserts the absence, so a release that ADDS the # attribute is a red rather than a silent divergence from this note. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # On x86-64 (CPython 3.12.3), against THIS EXACT PROBE, twice: # (a) the 2.4.0 sdist the recipe's SRC_URI sha256 pins # (aab9e56047c8...b7a, verified against the downloaded tarball), unpacked, # its src/ on PYTHONPATH -- not a pip copy and not upstream git; # (b) THE BYTES THIS RECIPE BUILDS -- python3-termcolor_2.4.0-r0_wasm32.deb, # extracted, its site-packages on PYTHONPATH. # (a) and (b) agreed on EVERY key except the three that name a path or read the # installed metadata (`module_file', `impl_file', `dist_version' -- (a) has no # dist-info to read at all). Recorded 2026-09-01; the ticket's design doc carries # the transcript. # # THE CONTROL INTERPRETER IS THE IMAGE'S OWN MINOR VERSION: CPython 3.12 on both # sides, unlike 99-python3-elementpath, which had to state a 3.10-against-3.12 # gap. Everything asserted below is content-determined -- integers from three # frozen dicts, `%'-formatted into a string -- and none of it reads the clock, # the locale, hash randomisation, pointer width or byte order. The on-target run # has still not been performed anywhere: the batch verify is the first. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE TTY PATH. `colored' colouring because stdout IS a terminal is the one # branch of `_can_do_colour' that cannot be reached from a suite whose # output is captured to a file. `notty-isatty' and `notty-plain' assert the # branch that IS reached, and state which one that is, rather than leaving a # reader to assume the other was exercised. # - `python3 -m termcolor'. `__main__.py' prints the whole table to a terminal # for a human to look at; run non-interactively it emits sixty-odd plain # lines and asserts nothing the cases below do not. It is asserted to SHIP # (`pkg-main') and not run. # - termcolor's OWN test suite (tests/ in the sdist). The recipe does not # inherit ptest and does not install them, and running them would need # pytest, which this image does not carry. # - The 256-colour and truecolor forms. 2.4.0 has neither; `colored' formats # one integer from one of three dicts, and there is nothing else in it. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-termcolor # THE CANARY, which asserts nothing about python3-termcolor. # # 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/termcolor DI=$SITE/termcolor-2.4.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-termcolor.list PROBE=$BST_DATA/python3-termcolor-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-termcolor.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import termcolor' 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" # THE FOUR MODULES, EACH NAMED. This is a PACKAGE, not a single module, and the # split is load-bearing: `__init__.py' re-exports from `termcolor.py', which is # where the whole implementation lives, and `_types.py' carries the Literal # aliases that `termcolor.py' imports AT MODULE SCOPE. A wheel that dropped # `_types.py' would leave the directory present and `import termcolor' raising -- # so one case each, and a red names which file went. check $P/pkg-init test -f "$PKGDIR/__init__.py" check $P/pkg-impl test -f "$PKGDIR/termcolor.py" check $P/pkg-types test -f "$PKGDIR/_types.py" # `__main__.py' is the `python3 -m termcolor' demo. Asserted to ship, not run -- # see WHAT IS NOT COVERED in the header. check $P/pkg-main test -f "$PKGDIR/__main__.py" # THE package_data MARKER. `py.typed' is what makes the Literal aliases in # `_types.py' visible to a type checker; it is a zero-byte-ish data file, which # is exactly the kind of payload a wheel build drops silently while every .py # lands, and NOTHING the behavioural cases below do would notice. check $P/pkg-py-typed test -f "$PKGDIR/py.typed" # ...AND IT IS A DIRECTORY, NOT A FLAT MODULE. A release that collapsed to a # single `termcolor.py' at the top of site-packages would still import and would # still pass most cases below, while the shape this image ships had changed. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/termcolor.py" ]; then echo present; else echo absent; fi)" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on # this image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- loudly, here, instead of at `import' later. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON, ITS METADATA, OR THE DECLARED # package_data. 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. # # 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 ;; */py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata, which for THIS package is the ONLY statement of the # version there is -- see the header: there is no `termcolor.__version__'. The # directory name carries it a second time in $DI itself, and `$P/dist-version' # below reads it a third way, through importlib.metadata at runtime. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 2.4.0" cat "$DI/METADATA" # WHEEL names the backend that built it. This recipe inherits # `python_setuptools_build_meta' but pyproject.toml declares # `build-backend = "hatchling.build"', so what actually ran is HATCHLING -- and # that is the fact `Generator:' records. A build in which the pyproject backend # had been ignored and setuptools had run instead would produce a working package # and change this line, which is the only place the substitution is visible. check_out $P/dist-info-generator "hatchling" cat "$DI/WHEEL" # RECORD is the wheel's own manifest of what it installed. It ships, and it is # what a `pip check'-shaped tool would read; asserted present rather than parsed. check $P/dist-info-record test -f "$DI/RECORD" # The licence text, which the recipe's LIC_FILES_CHKSUM pins by md5 at build # time. hatchling puts it under dist-info/licenses/, NOT at the dist-info root, # and a case looking in the wrong place would red on a correct package. check $P/dist-info-license test -f "$DI/licenses/COPYING.txt" # NO top_level.txt, and that is not an omission. It is a setuptools artefact; # hatchling does not write one, so its ABSENCE is what a hatchling-built wheel # looks like and its presence would mean something else built this. check_eq $P/no-top-level "absent" \ "$(if [ -f "$DI/top_level.txt" ]; then echo present; else echo absent; fi)" # --------------------------------------------------------------- the package -- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-termcolor 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 -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-termcolor$/,/^$/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, WHOLE. Unlike every other python3 port's suite # this one asserts the ENTIRE list rather than one case per name, because the # list is ONE name: `Depends: python3-core'. Asserting only that python3-core # appears would pass just as well on a package that had grown a second, unmet # dependency -- and for a package whose claim is that it needs nothing but the # interpreter, the closed statement IS the assertion. 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")" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If termcolor 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 termcolor' # ...and the two callables by name, which is the import-level statement that the # package's public surface is bound. It separates "the module is absent" from # "the module is present and its re-exports are wrong". check $P/api-import python3 -c 'from termcolor import colored, cprint' # ...and the implementation submodule separately, because `__init__.py' imports # from `termcolor.termcolor', which imports from `._types' at module scope: a # missing `_types.py' presents AS a broken termcolor and this case is what tells # the two apart. check $P/impl-import python3 -c 'from termcolor.termcolor import COLORS' # ---------------------------------------------------------------- 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 `^' and `=' anchors keep `fg_grey' # distinguishable from `bg_on_grey' and `no_color_env' from # `no_color_env_restored'. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE VERSION, from the installed metadata read at runtime -- the third and last # of the three independent statements ($DI's own name and its METADATA line being # the other two). See the header: the module carries no `__version__', so this is # the case a mis-resolved hatch-vcs version source reds on. check_eq $P/dist-version "2.4.0" "$(pv dist_version)" check_eq $P/no-dunder-version "no" "$(pv has_dunder_version)" # 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 termcolor. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/impl-answered "$PKGDIR/termcolor.py" "$(pv impl_file)" # The interpreter the port was built for. `site-packages' is version-stamped in # the path this suite asserts against, so a python3 minor bump that moved it # would red here rather than in a stack of confusing payload reds. check_eq $P/py-version "3.12" "$(pv py_version)" # THE PUBLIC SURFACE, WHOLE. termcolor DOES declare `__all__', so this is the # package's own statement of what it exports rather than dir()'s report of what # happens to be bound -- a real API assertion. Note it names the three TABLES and # RESET alongside the two functions: they are documented API here, which is why # the cases below may read them directly. check_eq $P/api-surface \ "ATTRIBUTES,COLORS,HIGHLIGHTS,RESET,colored,cprint" \ "$(pv api_surface)" # ------------------------------------------------------------ the primitives -- # RESET, on its own. Every wrapped result below ends in it, so a build in which # RESET were wrong would red on all forty-one table cases and name no cause; this # case is the one that names it. check_eq $P/reset "ESC[0m" "$(pv reset)" # THE THREE TABLES, WHOLE AND AS DATA. `colored' only ever looks a name up in one # of these dicts and interpolates the integer, so the tables ARE the package: the # case-by-case assertions below say the lookup and the formatting work, and these # three say the mapping itself is complete and unrenumbered. They are also where # the two BACKWARDS-COMPATIBILITY ALIASES are visible: `grey' is 30, the same as # `black', and `on_grey' is 40, the same as `on_black'. Upstream keeps both names # deliberately, and a table case is the only place that fact is legible. check_eq $P/colors-table \ "black:30,blue:34,cyan:36,dark_grey:90,green:32,grey:30,light_blue:94,light_cyan:96,light_green:92,light_grey:37,light_magenta:95,light_red:91,light_yellow:93,magenta:35,red:31,white:97,yellow:33" \ "$(pv colors_table)" check_eq $P/highlights-table \ "on_black:40,on_blue:44,on_cyan:46,on_dark_grey:100,on_green:42,on_grey:40,on_light_blue:104,on_light_cyan:106,on_light_green:102,on_light_grey:47,on_light_magenta:105,on_light_red:101,on_light_yellow:103,on_magenta:45,on_red:41,on_white:107,on_yellow:43" \ "$(pv highlights_table)" check_eq $P/attributes-table \ "blink:5,bold:1,concealed:8,dark:2,reverse:7,underline:4" \ "$(pv attributes_table)" # ------------------------------------------------------- the colour table ---- # EVERY NAME, ONE CASE EACH, BYTE FOR BYTE, against the sequence the README # fixes. The expectations are written out in full rather than computed from the # tables above, so a build in which BOTH the table and the formatting had shifted # together -- which is what a wholesale renumbering looks like -- still reds. # # Seventeen foreground names. The eight bright ones are 90-97 rather than 30-37, # which is the half of the table an implementation that only handled the original # eight would get wrong. check_eq $P/fg-black "ESC[30mXESC[0m" "$(pv fg_black)" check_eq $P/fg-grey "ESC[30mXESC[0m" "$(pv fg_grey)" check_eq $P/fg-red "ESC[31mXESC[0m" "$(pv fg_red)" check_eq $P/fg-green "ESC[32mXESC[0m" "$(pv fg_green)" check_eq $P/fg-yellow "ESC[33mXESC[0m" "$(pv fg_yellow)" check_eq $P/fg-blue "ESC[34mXESC[0m" "$(pv fg_blue)" check_eq $P/fg-magenta "ESC[35mXESC[0m" "$(pv fg_magenta)" check_eq $P/fg-cyan "ESC[36mXESC[0m" "$(pv fg_cyan)" check_eq $P/fg-light-grey "ESC[37mXESC[0m" "$(pv fg_light_grey)" check_eq $P/fg-dark-grey "ESC[90mXESC[0m" "$(pv fg_dark_grey)" check_eq $P/fg-light-red "ESC[91mXESC[0m" "$(pv fg_light_red)" check_eq $P/fg-light-green "ESC[92mXESC[0m" "$(pv fg_light_green)" check_eq $P/fg-light-yellow "ESC[93mXESC[0m" "$(pv fg_light_yellow)" check_eq $P/fg-light-blue "ESC[94mXESC[0m" "$(pv fg_light_blue)" check_eq $P/fg-light-magenta "ESC[95mXESC[0m" "$(pv fg_light_magenta)" check_eq $P/fg-light-cyan "ESC[96mXESC[0m" "$(pv fg_light_cyan)" check_eq $P/fg-white "ESC[97mXESC[0m" "$(pv fg_white)" # Seventeen background names, each 10 above its foreground -- 40-47 and 100-107. # The offset is what a background implementation gets wrong by reusing the # foreground table, and asserting the whole set is what catches it on the one # name that was missed. check_eq $P/bg-on-black "ESC[40mXESC[0m" "$(pv bg_on_black)" check_eq $P/bg-on-grey "ESC[40mXESC[0m" "$(pv bg_on_grey)" check_eq $P/bg-on-red "ESC[41mXESC[0m" "$(pv bg_on_red)" check_eq $P/bg-on-green "ESC[42mXESC[0m" "$(pv bg_on_green)" check_eq $P/bg-on-yellow "ESC[43mXESC[0m" "$(pv bg_on_yellow)" check_eq $P/bg-on-blue "ESC[44mXESC[0m" "$(pv bg_on_blue)" check_eq $P/bg-on-magenta "ESC[45mXESC[0m" "$(pv bg_on_magenta)" check_eq $P/bg-on-cyan "ESC[46mXESC[0m" "$(pv bg_on_cyan)" check_eq $P/bg-on-light-grey "ESC[47mXESC[0m" "$(pv bg_on_light_grey)" check_eq $P/bg-on-dark-grey "ESC[100mXESC[0m" "$(pv bg_on_dark_grey)" check_eq $P/bg-on-light-red "ESC[101mXESC[0m" "$(pv bg_on_light_red)" check_eq $P/bg-on-light-green "ESC[102mXESC[0m" "$(pv bg_on_light_green)" check_eq $P/bg-on-light-yellow "ESC[103mXESC[0m" "$(pv bg_on_light_yellow)" check_eq $P/bg-on-light-blue "ESC[104mXESC[0m" "$(pv bg_on_light_blue)" check_eq $P/bg-on-light-magenta "ESC[105mXESC[0m" "$(pv bg_on_light_magenta)" check_eq $P/bg-on-light-cyan "ESC[106mXESC[0m" "$(pv bg_on_light_cyan)" check_eq $P/bg-on-white "ESC[107mXESC[0m" "$(pv bg_on_white)" # Six attributes. `dark' is SGR 2 (faint) and `concealed' is 8 -- neither is a # colour, and both go through the same third table. check_eq $P/attr-bold "ESC[1mXESC[0m" "$(pv attr_bold)" check_eq $P/attr-dark "ESC[2mXESC[0m" "$(pv attr_dark)" check_eq $P/attr-underline "ESC[4mXESC[0m" "$(pv attr_underline)" check_eq $P/attr-blink "ESC[5mXESC[0m" "$(pv attr_blink)" check_eq $P/attr-reverse "ESC[7mXESC[0m" "$(pv attr_reverse)" check_eq $P/attr-concealed "ESC[8mXESC[0m" "$(pv attr_concealed)" # ------------------------------------------------------------ the composition -- # THE ORDER IS NOT ARGUMENT ORDER, AND THIS IS THE ONE THING A READER # RECONSTRUCTING `colored' FROM ITS SIGNATURE GETS WRONG. Each layer PREPENDS to # the result so far: the colour goes on first and ends up INNERMOST, the # highlight wraps it, and the attributes wrap that -- in REVERSE list order, # because each prepends in turn. So `colored("X", "red", "on_green", ["bold", # "underline"])' is underline, bold, on_green, red, X, reset -- exactly backwards # from how it reads. check_eq $P/combo "ESC[4mESC[1mESC[42mESC[31mXESC[0m" "$(pv combo)" # ...and the same call with the attribute list REVERSED, which must swap those # two sequences and nothing else. Asserting one ordering alone would pass just as # well if the list were being sorted, or reduced to a set, or applied in a fixed # canonical order -- and each of those is a real way a reimplementation diverges. check_eq $P/combo-attrs-reversed "ESC[1mESC[4mESC[42mESC[31mXESC[0m" \ "$(pv combo_attrs_reversed)" # NO colour, NO highlight, NO attributes -- and `colored' STILL appends RESET. It # is not a no-op, which is what a caller passing `color=None' to mean "leave this # one alone" would assume, and it is why a loop that colours conditionally still # emits a reset for the uncoloured branch. check_eq $P/bare-reset "XESC[0m" "$(pv bare_reset)" # `text' is `str(text)'-ed, so a non-string argument is formatted rather than # refused. The annotation says `object' and this case is what that means. check_eq $P/non-str-text "ESC[31m123ESC[0m" "$(pv non_str_text)" # An unknown name is a KeyError straight out of the table lookup: there is no # validation layer and no fallback to plain text. A caller's typo fails loudly, # and these two cases are what say so rather than leaving it to be discovered by # whatever ports next. check_eq $P/bad-color-raised "yes" "$(pv bad_color_raised)" check_eq $P/bad-color-error-type "KeyError" "$(pv bad_color_error_type)" # -------------------------------------------------------------------- cprint -- # `cprint' is `print(colored(...))' and nothing else, so what it must produce is # EXACTLY what `colored' returns plus the newline `print' adds -- captured here # through a redirected StringIO, which is the only way to say that on target. # The trailing `\n' in the expectation is the two characters backslash-n: the # probe escapes newlines on the way out, because an embedded one would land in # the capture as a second, unkeyed line. check_eq $P/cprint-captured 'ESC[1mESC[42mESC[31mXESC[0m\n' "$(pv cprint_captured)" # ...and the identity asserted by the probe itself, against a `colored' call it # made in the same interpreter. The case above pins the bytes; this one says the # two functions agree, which is the property that survives a table change. check_eq $P/cprint-is-colored-plus-newline "yes" \ "$(pv cprint_is_colored_plus_newline)" # ------------------------------------------------------------- suppression ---- # THE DOCUMENTED SUPPRESSION PATHS, AND THEY ARE A PRECEDENCE QUESTION RATHER # THAN A FLAG. Every case above ran with FORCE_COLOR set, which is what makes # these mean something: each asserts that a suppression beats an ACTIVE request # for colour. Asserting only that `colored' returns plain text under NO_COLOR # would pass identically on a build where colour never worked at all -- and on # this probe, whose stdout is a file, that is the DEFAULT state, so it is not a # hypothetical failure mode. # # `-restored' after each is the control: the same call with the variable removed # must colour again. Without it a red anywhere in this block could not be told # from the environment having been left dirty by the case before. check_eq $P/ansi-colors-disabled "X" "$(pv ansi_colors_disabled)" check_eq $P/ansi-colors-disabled-restored "ESC[31mXESC[0m" "$(pv ansi_colors_disabled_restored)" check_eq $P/no-color-env "X" "$(pv no_color_env)" check_eq $P/no-color-env-restored "ESC[31mXESC[0m" "$(pv no_color_env_restored)" # TERM=dumb is checked AFTER FORCE_COLOR in `_can_do_colour', so it does NOT beat # it and the result here is COLOURED. That is the case which says the order in # that function is the documented one rather than "any of these switches it off" # -- and it is the one a reader would predict wrongly. check_eq $P/term-dumb-does-not-beat-force "ESC[31mXESC[0m" "$(pv term_dumb)" check_eq $P/term-dumb-restored "ESC[31mXESC[0m" "$(pv term_dumb_restored)" # THE KEYWORDS OUTRANK THE ENVIRONMENT, in both directions. no-color.org's own # text asks for exactly this: a per-instance argument overrides $NO_COLOR. These # two separate "the environment is consulted" from "the environment is obeyed # unconditionally", and they are the reason the probe drives the cases above # through the environment rather than through these keywords. check_eq $P/force-color-kw-beats-no-color-env "ESC[31mXESC[0m" \ "$(pv force_color_kw_beats_no_color_env)" check_eq $P/no-color-kw-beats-force-color-env "X" \ "$(pv no_color_kw_beats_force_color_env)" # ------------------------------------------------------- the tty gate itself -- # With FORCE_COLOR removed the probe's stdout is a plain FILE and `colored' # returns the text unwrapped. This is the package's DEFAULT behaviour on target # -- a program whose output is piped or redirected gets no escapes -- and it is # the fact every case above had to work around. It is asserted rather than left # implicit, because a build in which the tty test wrongly answered TRUE would # leak escape sequences into every redirected log on this image, and nothing else # here would notice. check_eq $P/notty-isatty "no" "$(pv notty_isatty)" check_eq $P/notty-plain "X" "$(pv notty_plain)" # ------------------------------------------------------- the deprecated name -- # `__ALL__' is served by a module-level `__getattr__' that warns and returns a # two-name list; 2.4.0 announces its removal in termcolor 3. Asserted so that the # version bump which drops it reds HERE rather than as an AttributeError in # whatever on this image still spells it that way. The WARNING is a case of its # own: a release that kept the name and dropped the deprecation notice would pass # the first and red the second. check_eq $P/dunder-ALL "colored,cprint" "$(pv dunder_ALL)" check_eq $P/dunder-ALL-warning "DeprecationWarning" "$(pv dunder_ALL_warning)" # ...and an attribute that is NEITHER `__ALL__' nor real must still raise, rather # than being swallowed by that same `__getattr__'. A module hook that returned # something for every name would pass the case above and break every # `hasattr'-based feature test on the image. check_eq $P/unknown-attr-raised "yes" "$(pv unknown_attr_raised)" # 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"