. "$BST_LIB" # BST-TIMEOUT: 1800 # FOUR 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 builds about thirty short strings and # # flips two process-wide flags, which is nothing beside the # # interpreter load. # # # # 1800 RATHER THAN 1200, matching 99-python3-jsonref for the # # same reason: `import ctypes', `import colorclass' and # # `import colorclass.windows' are each driven on their own # # before the probe, and each is a full load of one large # # static image. The three are worth their starts -- see `the # # boundary' below, where they are the three DIFFERENT # # statements that tell this port's success from its failure. # # # # 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-colorclass 2.2.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-colorclass_2.2.2.bb -- `inherit pypi # setuptools3', PyPI sdist, carrying meta-brintos' # 0001-windows-define-the-Win32-structures-only-when-ctypes-imports.patch) on the # wasm32-hwjs glibc image -- COLOURING BY STRING SUBCLASS. # # ============================================================================ # WHAT THIS PORT ADDS, AND WHY IT IS NOT A print() WRAPPER. # ============================================================================ # # `Color('{red}Test{/red}')' is a str SUBCLASS, not a function that returns an # escaped string, and that is the whole design. The value carries its markup and # renders to escapes -- and it reports its VISIBLE length, so text that has been # coloured still lines up in a table or a fixed-width field. `len()' of the # rendered value is 4 while the bytes are 14, and `red-len' / `red-len-raw' below # are asserted TOGETHER for that reason: either alone would pass on a library # that had done only half the job. # # The three groups of cases follow the three halves of that: what the value # RENDERS to, what it MEASURES as, and what the process-wide toggles # (disable_all_colors, set_light_background) do to both. # # ============================================================================ # THE BOUNDARY THIS PORT IS ABOUT: ctypes. # ============================================================================ # # colorclass/__init__.py does `from colorclass.windows import Windows' # UNCONDITIONALLY, and colorclass/windows.py does a module-scope `import ctypes'. # So on a stock tree `import colorclass' is exactly as importable as ctypes is -- # there is no arm of this package that avoids it. # # _ctypes is configured out of this image's interpreter because ffi_closure_alloc # will not link on this arm (hwjs-cc#15), so `import ctypes' raises # ModuleNotFoundError here and an unpatched colorclass could not be imported at # all. meta-brintos patches the three Win32 structure declarations -- the only # things that need ctypes at import time, because they subclass ctypes.Structure # at class-creation time -- behind a guard. # # THREE CASES SAY THAT, AND THEY ARE DELIBERATELY SEPARATE: # # ctypes-refused -- ctypes still does not import. A RED WITNESS: it names the # platform gap, and the day hwjs-cc#15 closes it changes # value and says so. # import -- colorclass imports ANYWAY. This is the port. # windows-structs -- and it did so by not defining the Win32 structures, # rather than because ctypes quietly came back. # # Reading any one of them alone gets this port wrong. Together they say which of # the two shapes shipped. # # ============================================================================ # THE ASSERTIONS ARE MEASURED, NOT PREDICTED -- ON x86-64, NOT ON THIS ARM. # ============================================================================ # # Every literal below was measured against the sdist the recipe's SRC_URI sha256 # pins (6d4fe287...4366, verified by digest before anything was measured), with # this layer's patch applied and _ctypes blocked by a meta_path hook that raises # ModuleNotFoundError exactly as this image does. The control interpreter is # CPython 3.12.3 and this image ships 3.12, so there is no minor-version gap to # discount -- unlike several neighbouring python suites here. # # NO on-target result exists yet. This port's first run on wasm32-hwjs is the # batch verify that this suite is part of. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, ONE PACKAGE, NINE MODULES. # ============================================================================ # # 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. # # colorclass ships NO C: the sdist holds nine .py files, a LICENSE, a README, a # PKG-INFO, a setup.py and a pyproject.toml, and not one .c, .pyx or .so. # `pkg-no-so' below is therefore a REGRESSION TRIPWIRE on a version bump rather # than a live risk. setup.py declares `packages = ['colorclass']', so # site-packages holds a DIRECTORY -- and `import colorclass' succeeds while one # of its submodules is missing, so each is named: a dropped parse.py or search.py # would present as a WRONG RENDER rather than as a failing import. # # ============================================================================ # THE VERSION IS TWO DIFFERENT NUMBERS, AND BOTH ARE CORRECT. # ============================================================================ # # colorclass/__init__.py carries `__version__ = '2.2.0'' while setup.py, PKG-INFO # and the recipe filename all say 2.2.2. That is upstream's own inconsistency, in # the released 2.2.2 sdist, and not something this image does to it. Both are # asserted, at their own values -- `version' 2.2.0 from the module and # `dist-version' 2.2.2 from the installed metadata. Asserting either as "the # version" is how a correct install gets reported as a mixed one. # The runner's first emitted line is eaten on this platform # (linux-in-the-browser#177), and 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. P=python3-colorclass bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/colorclass DI=$SITE/colorclass-2.2.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-colorclass.list PROBE=$BST_DATA/python3-colorclass-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-colorclass.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import colorclass' 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 "$PKG" # ...AND IT IS A DIRECTORY, NOT A FLAT MODULE. A release that collapsed the # package into one colorclass.py 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/colorclass.py" ]; then echo present; else echo absent; fi)" # EACH SUBMODULE BY NAME. See the header: `import colorclass' reaches codes, # color, core, toggles and windows through __init__, and parse and search only # indirectly -- so a missing parse.py presents as a wrong render, not as an # ImportError. __main__.py is what makes `python3 -m colorclass' work and is # reached by neither. for _m in __init__ __main__ codes color core parse search toggles windows; do check $P/module-$_m test -f "$PKG/$_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)" # THIS PACKAGE SHIPS NO EXECUTABLE. pyproject.toml declares an EMPTY # `[tool.poetry.scripts]' and setup.py passes no `scripts=', so there is no entry # point to generate and nothing of this package's should land in /usr/bin. It is # driven as `python3 -m colorclass' instead, which is __main__.py's job. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/bin/|^/usr/sbin/' "$W/pkgf" 2>/dev/null)" # ...AND EVERYTHING IT SHIPS IS UNDER site-packages. A file outside it would be # this recipe writing somewhere it does not own. # # `/.' is dpkg's own root-directory entry and heads every package's .list, so it # is not this recipe writing outside its tree. _bad="" while IFS= read -r _f; do case "$_f" in ""|/|/.|/usr|/usr/lib|/usr/lib/python3.12|"$SITE"|"$SITE"/) continue ;; "$SITE"/*) continue ;; /usr/share/doc/*) continue ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-site-packages "" "$_bad" # ----------------------------------------------------------- the dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" # 2.2.2 HERE AND 2.2.0 FROM THE MODULE. See the header -- both are correct. check_out $P/dist-info-version "Version: 2.2.2" cat "$DI/METADATA" check_out $P/dist-info-name "Name: colorclass" cat "$DI/METADATA" check_out $P/dist-info-top-level "colorclass" cat "$DI/top_level.txt" # ------------------------------------------------------------- dpkg's side --- sed -n '/^Package: python3-colorclass$/,/^$/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" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" # THE DEPENDENCY THAT RESOLVES AND STILL DOES NOT IMPORT. The meta-python recipe # declares python3-ctypes, and this image HAS that package -- it ships Lib/ctypes # -- so the dependency is satisfied while `import ctypes' still fails on the # missing _ctypes extension. Asserted next to `ctypes-refused' below, because the # PAIR is the fact; either half alone reads as a broken image. check_out $P/pkg-depends-ctypes "python3-ctypes" cat "$W/dep" # ------------------------------------------------------------ the boundary --- # THE RED WITNESS. `import ctypes' must still fail, and the message is asserted # in full rather than by exit status: failing for some OTHER reason -- a missing # python3-ctypes, a broken stdlib -- would also be non-zero and would read as # this documented boundary while being an unrelated defect. # # The day hwjs-cc#15 closes and _ctypes builds, this case goes red with # got:'' and the layer's patch can be revisited. That is the signal. check_eq $P/ctypes-refused \ "ModuleNotFoundError: No module named '_ctypes'" \ "$(python3 -c 'import ctypes' 2>&1 | tail -1)" # ...AND colorclass IMPORTS ANYWAY. THIS IS THE PORT. Unpatched it could not, # because __init__ pulls in windows.py which imports ctypes at module scope. check $P/import python3 -c 'import colorclass' # ...and windows.py itself imports, which is the module the patch is in. A # separate start from the one above, because `import colorclass' reaching it # through __init__ and it importing on its own are two different statements, and # the patch could satisfy one without the other. check $P/windows-import python3 -c 'import colorclass.windows' # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "missing $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE start does the whole of the behaviour. See the header on the cost of a # python3 start on this arm. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # pv : the probe's value for , or '' if the probe never got there. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # ESC IS SPELLED `ESC' IN EVERY EXPECTATION BELOW. The probe renders it that way # on purpose -- a raw 0x1b would travel through this shell, _bst_1line and the # runner's line parser before being compared, and would print a control character # into the capture on a red. See the probe's module docstring. # ------------------------------------------------------- identity and version check_eq $P/version "2.2.0" "$(pv version)" check_eq $P/dist-version "2.2.2" "$(pv dist_version)" check_eq $P/py-version "3.12" "$(pv py_version)" check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/author "@Robpol86" "$(pv author)" check_eq $P/license "MIT" "$(pv license)" # The package's OWN statement of what it exports. check_eq $P/api-surface \ "Color,Windows,disable_all_colors,enable_all_colors,is_enabled,is_light,list_tags,set_dark_background,set_light_background" \ "$(pv api_surface)" # Every submodule imported on its own. See the header: __init__ does not reach # parse or search directly. check_eq $P/submodules "codes,color,core,parse,search,toggles,windows" \ "$(pv submodules)" # --------------------------------------------- the boundary, from the inside # The probe's own view of the two cases asserted from the shell above, plus the # one only it can see: WHICH SHAPE windows.py took. With ctypes absent the # patch binds the name to None and defines no structures. check_eq $P/probe-ctypes-importable "no" "$(pv ctypes_importable)" check_eq $P/probe-ctypes-error "No module named '_ctypes'" "$(pv ctypes_error)" check_eq $P/windows-ctypes-none "yes" "$(pv windows_ctypes_none)" check_eq $P/windows-structs "" "$(pv windows_structs)" # ...AND THE WINDOWS ENTRY POINTS STILL ANSWER, which is what says the patch # removed the import and not the behaviour. Each returns False before reaching # anything ctypes-backed, on this platform, by upstream's own guard. check_eq $P/is-windows "no" "$(pv is_windows)" check_eq $P/windows-enable "False" "$(pv windows_enable)" check_eq $P/windows-disable "False" "$(pv windows_disable)" check_eq $P/windows-is-enabled "False" "$(pv windows_is_enabled)" # ------------------------------------------------------------- what it RENDERS # A Color IS A str, which is the design the whole library rests on: a coloured # value can be handed to anything that takes a string. check_eq $P/red-is-str "yes" "$(pv red_is_str)" check_eq $P/red-type "Color" "$(pv red_type)" # The documented example, from the package's own README. check_eq $P/red-render "ESC[31mTestESC[39m" "$(pv red_render)" # The two accessors the ticket names: the same value with and without its colour. check_eq $P/red-value-colors "ESC[31mTestESC[39m" "$(pv red_value_colors)" check_eq $P/red-value-no-colors "Test" "$(pv red_value_no_colors)" check_eq $P/red-has-colors "yes" "$(pv red_has_colors)" # ------------------------------------------------------------- what it MEASURES # THE PROPERTY THE LIBRARY EXISTS FOR, and the three numbers are asserted # together: len() is the VISIBLE length, the raw bytes are more than three times # that, and the two agree once the colour is stripped. `red-len' alone would pass # on a library that had never added the escapes at all. check_eq $P/red-len "4" "$(pv red_len)" check_eq $P/red-len-raw "14" "$(pv red_len_raw)" check_eq $P/red-len-plain "4" "$(pv red_len_plain)" # ...AND THE str METHODS SEE THE TEXT, not the escapes. README's own example. check_eq $P/red-istitle "yes" "$(pv red_istitle)" # upper() colours survive and the TEXT is uppercased -- the escapes are not # mangled by a method that knows nothing about them. check_eq $P/red-upper "ESC[31mTESTESC[39m" "$(pv red_upper)" # A SECOND COLOUR AND A BACKGROUND TAG, so the mapping is asserted rather than # one constant. Each closing tag resets only its OWN half -- /red is 39, the # default foreground, and /bggreen is 49, the default background, not a blanket # reset -- which is what lets tags nest. check_eq $P/blue-render "ESC[34mxyESC[39m" "$(pv blue_render)" check_eq $P/bggreen-render "ESC[42mxyESC[49m" "$(pv bggreen_render)" # NESTED TAGS MERGE INTO ONE SEQUENCE rather than emitting two. This is the case # that says the parser builds a code set instead of concatenating escapes. check_eq $P/nested-render "ESC[31;42mxyESC[39;49m" "$(pv nested_render)" # UNTAGGED TEXT PASSES THROUGH UNTOUCHED and reports has_colors False -- the case # that says the parser is not decorating everything handed to it. check_eq $P/plain-render "plain" "$(pv plain_render)" check_eq $P/plain-has-colors "no" "$(pv plain_has_colors)" check_eq $P/plain-len "5" "$(pv plain_len)" # AN UNKNOWN TAG IS LEFT ALONE rather than raising or being eaten. A caller # formatting arbitrary text through this library must not have `{foo}' silently # removed from it. check_eq $P/unknown-tag "{nosuchcolor}xy{/nosuchcolor}" "$(pv unknown_tag)" # keep_tags=True IS THE DOCUMENTED OPT-OUT: the markup survives as text, no # escape is emitted, and len() is the length of what is there -- 15, the tagged # form -- which is what a caller wants when the value is stored rather than # printed. check_eq $P/keep-tags-render "{red}Test{/red}" "$(pv keep_tags_render)" check_eq $P/keep-tags-len "15" "$(pv keep_tags_len)" # ------------------------------------------------------------------- colorize # The wrapper-function half of the API, asserted to agree EXACTLY with the tag # written by hand -- otherwise the two halves of the interface could drift apart # with each looking correct on its own. check_eq $P/colorize-red "ESC[31mTestESC[39m" "$(pv colorize_red)" check_eq $P/colorize-matches-tag "yes" "$(pv colorize_matches_tag)" check_eq $P/colorize-len "4" "$(pv colorize_len)" # ...and the per-colour classmethods it backs. check_eq $P/classmethod-red "ESC[31mTestESC[39m" "$(pv classmethod_red)" check_eq $P/classmethod-green "ESC[32mTestESC[39m" "$(pv classmethod_green)" # ------------------------------------------------------------- the auto tags # THE `auto' TAGS PICK BY BACKGROUND, and that choice is a PROCESS-WIDE TOGGLE # rather than a property of the string. The SAME tag is rendered under both # settings, so these cases say the switch MOVED the colour rather than only that # some colour came out: 91 (bright red, legible on a dark background) is the # default, and 31 (plain red) is what a light background selects. check_eq $P/auto-default-is-light "no" "$(pv auto_default_is_light)" check_eq $P/autored-default "ESC[91mTestESC[39m" "$(pv autored_default)" check_eq $P/is-light-after-set-light "yes" "$(pv is_light_after_set_light)" check_eq $P/autored-light "ESC[31mTestESC[39m" "$(pv autored_light)" check_eq $P/autobgred-light "ESC[41mxyESC[49m" "$(pv autobgred_light)" # ...AND IT IS REVERSIBLE, which is what says set_light_background flipped a flag # rather than rewriting the code table. check_eq $P/is-light-after-set-dark "no" "$(pv is_light_after_set_dark)" check_eq $P/autored-dark "ESC[91mTestESC[39m" "$(pv autored_dark)" check_eq $P/autobgred-dark "ESC[101mxyESC[49m" "$(pv autobgred_dark)" # THE auto TAG DIFFERS FROM THE PLAIN ONE, which is what says `auto' resolved to # something rather than falling through to the fixed colour. check_eq $P/autored-differs-from-red "yes" "$(pv autored_differs_from_red)" check_eq $P/colorize-auto "ESC[91mTestESC[39m" "$(pv colorize_auto)" # ------------------------------------------------------------------- list_tags # The package's own catalogue of what it accepts. The COUNT and one fixed row are # asserted; the whole table is not, because that would pin every future tag # addition into a case about this port. check_eq $P/tags-count "60" "$(pv tags_count)" # The row carries both backticks-free single quotes, so it is built with the # '\'' idiom rather than double-quoted. _red_row='('\''red'\'', '\''/red'\'', 31, 39)' check_eq $P/tags-red-row "$_red_row" "$(pv tags_red_row)" check_eq $P/tags-has-autored "yes" "$(pv tags_has_autored)" # ------------------------------------------------- disable_all_colors / enable # THE GLOBAL OFF SWITCH, and the half that matters is that it STRIPS rather than # hides: a value built while colours are disabled carries no escape at all and # its len() is unchanged, so a program that turns colour off gets plain text # through the same code path rather than a different one. check_eq $P/is-enabled-before "yes" "$(pv is_enabled_before)" check_eq $P/is-enabled-after-disable "no" "$(pv is_enabled_after_disable)" check_eq $P/disabled-render "Test" "$(pv disabled_render)" check_eq $P/disabled-len "4" "$(pv disabled_len)" check_eq $P/disabled-has-colors "no" "$(pv disabled_has_colors)" check_eq $P/disabled-value-colors "Test" "$(pv disabled_value_colors)" # EVERY render goes plain, including the auto tags and the colorize wrapper -- # which is the ticket's own requirement, and what says the switch is read at # render time rather than at one entry point. check_eq $P/disabled-autored "Test" "$(pv disabled_autored)" check_eq $P/disabled-colorize "Test" "$(pv disabled_colorize)" # ...AND IT IS REVERSIBLE, which says disable_all_colors set a flag rather than # damaging the code table. check_eq $P/is-enabled-after-enable "yes" "$(pv is_enabled_after_enable)" check_eq $P/reenabled-render "ESC[31mTestESC[39m" "$(pv reenabled_render)" # The probe reached its end. Every case above reads a key the probe emits before # this one, so a probe that died midway would red them individually with got:''; # this is the single case that says the run was complete. check_eq $P/probe-complete "yes" "$(pv probe_complete)" rm -rf "$W"