. "$BST_LIB" # BST-TIMEOUT: 1800 # SEVEN python3 starts, and they are most of the cost. # # # # The seven: a bare `import Cheetah', a second import whose # # STDERR is the assertion, the main probe (ONE start for 100 # # measurements), the upstream probe, and THREE launches of # # the shipped CLI through its own shebang -- # # `cheetah-compile', `cheetah fill' and `cheetah-analyze'. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps one probe transcript. # # # # THE UPSTREAM PROBE IS THE ONE THING HERE WHOSE COST IS NOT # # THE START. It runs 2125 of the author's own tests, each # # compiling a template: 2.0s on the x86-64 control, and the # # only part of this file that could plausibly grow into the # # bound. It is a SEPARATE probe with its own 900s bound for # # exactly that reason -- if it overruns it reds ten cases and # # the other ninety are already measured and reported. # # # # The main probe's own work is ~45 template compilations, # # 0.6s on the control. 1800 leaves ~250s per interpreter # # start, against the ~60s one has measured at on this arm, # # and nothing in this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-cheetah 3.2.6.post1 (meta-openembedded/meta-python # recipes-devtools/python/python3-cheetah_3.2.6.post1.bb, `inherit pypi # setuptools3', PyPI sdist `Cheetah3-3.2.6.post1.tar.gz') on the wasm32-hwjs # glibc image, plus this layer's python3-cheetah_%.bbappend and its one patch. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # Cheetah is a template engine that is also a CODE GENERATOR: a template is # parsed into Python source, that source is compiled into a class, and rendering # is calling a method on an instance of it. Everything else follows from that # one design -- `#extends' is real class inheritance, `#block' is a method a # subclass may replace, and `cheetah-compile' writes the generated module out to # disk so it can be imported like any other. # # Placeholders resolve through a `searchList' of namespaces rather than through # one dict, and the resolution is done by `Cheetah.NameMapper', which walks # mappings, attributes and indices with a uniform dotted syntax and CALLS # whatever it lands on if it is callable. # # ============================================================================ # THE C ACCELERATOR IS DECLINED HERE, AND THAT IS THIS PORT'S SUBJECT. # ============================================================================ # # Cheetah ships `Cheetah/c/_namemapper.c' beside the pure-Python NameMapper and # imports it over the top inside a `try'/`except', setting `C_VERSION' # accordingly. It BUILDS on this arm -- measured, with the stock recipe: 1603 of # 1603 tasks and a real wasm side module in the .deb. It cannot be LOADED: the # target `/usr/bin/python3.12' is one static binary exporting 35 symbols, none of # them `Py*', and there is no `libpython3.12.so' (BRINTOS-2046). # # So this layer's bbappend patches `SetupConfig.py' to leave `ext_modules' empty # for class-target, and the package ships pure Python. The reasoning, and why a # `.so' that installs and cannot import is treated as a defect rather than a # harmless spare, is in python3-cheetah_%.bbappend; BRINTOS-2038 and BRINTOS-2055 # took the same way out for the same reason. # # WHAT THAT COSTS IS EIGHT MEASUREMENTS, AND THE SUITE NAMES ALL EIGHT. Not a # guess: the probe below was run on the x86-64 control twice on 2026-09-02, once # against the sdist as shipped and once with `_namemapper' built, and the two # transcripts differ in exactly these places -- # # cver_c_version no / yes # cver_ext_import ModuleNot… / returned # cver_notfound_module Cheetah.NameMapper / NameMapper # cver_valueForName_module Cheetah.NameMapper / Cheetah._namemapper # stackframes_warning_count 1 / 0 # stackframes_setting_off yes / no # stackframes_src_prepends_locals yes / no # api_getvar_dollar NotFound / ValueError # # -- and NOWHERE ELSE. Every rendering case in this file, every directive, the # inheritance chain, the include, the failure paths and the generated-module # round trip answer identically through either implementation. That is the # statement the port rests on, and it is measured rather than asserted. # # ============================================================================ # WHAT `useStackFrames' OFF ACTUALLY CHANGES. # ============================================================================ # # `Compiler.py' reads `NameMapper.C_VERSION' when a `ModuleCompiler' is built # and, finding it False, warns on stderr and forces `useStackFrames' off -- # because the frame walk is what is painfully slow in Python. The consequence is # visible in the code the compiler WRITES: # # with the extension _v = VFFSL(SL, "a", True) # without it _v = VFSL([locals()]+SL+[globals(), builtin], "a", True) # # so the local namespace is consulted either way -- as a prepended dict here, # through the frame walk there. The `stackframes_*' cases pin the emitted form # rather than the presence of the names, because BOTH names are bound in every # generated module's preamble and their presence says nothing. # # ONE CALLER-VISIBLE DIVERGENCE COMES OUT OF IT: `getVar("$")' strips the sigil # and asks for the empty key. The C mapper raises `ValueError'; the pure-Python # `valueFromSearchList' raises `Cheetah.NameMapper.NotFound', a `LookupError' # that is NOT a `ValueError'. Four of upstream's own tests assert the C answer, # so they error here -- named one by one in `upstream-error-ids' rather than # tolerated as a count. A FIFTH upstream error is unrelated to the accelerator # and is stated at that same case: `ImportHooksTest.test_import_builtin', which # this arm's builtin module set reaches and the x86-64 control's does not. # # ============================================================================ # A NAME THAT IS ALSO A `dict' METHOD IS A TRAP ON BOTH ARMS. # ============================================================================ # # Because a local namespace is consulted first and `valueForName' falls back to # `getattr' when a key is absent, a placeholder named `items', `keys', `values', # `get', `copy' or `pop' resolves to that namespace dict's BOUND METHOD, which # the placeholder machinery then autocalls. `$items' renders a `dict_items' # repr, `$items[1]' is a `TypeError', and `#for $i in $items' iterates the local # namespace while the loop body assigns into it -- "dictionary changed size # during iteration". # # MEASURED ON BOTH ARMS on the control, so it belongs to Cheetah and to the # name, not to this image. It is pinned (`collide-*') because a template author # reaches for `items' constantly and nothing warns, and because a reader finding # it on target would otherwise spend the afternoon blaming the port. # # ============================================================================ # UPSTREAM'S OWN TESTS SHIP AND RUN. # ============================================================================ # # `SetupConfig.py' names the whole `Cheetah' tree, so `Cheetah/Tests/' lands in # site-packages and python3-unittest is on this image. 2125 tests execute for # the cost of one interpreter start. `CheetahWrapper' is excluded from the run -- # its 45 tests drive the CLI as a subprocess, which on this arm is tens of # seconds each; the CLI is covered instead by the three real launches below. # # The count is asserted separately from the result, because a collection that # silently found nothing reports zero failures and reads as green. P=python3-cheetah # THE CANARY, which asserts nothing about python3-cheetah. # # 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/Cheetah DI=$SITE/Cheetah3-3.2.6.post1.dist-info PKGLIST=/var/lib/dpkg/info/python3-cheetah.list PROBE=$BST_DATA/python3-cheetah-probe.py UPROBE=$BST_DATA/python3-cheetah-upstream.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-cheetah.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. The behavioural cases further down are the # functional statement; these say WHICH copy answered, so a package importing # out of some other path on sys.path is distinguishable from one importing out # of what this image installed. check $P/site-packages-dir test -d "$SITE" # A PACKAGE, AND THE PyPI NAME IS NOT THE IMPORT NAME: the distribution is # `Cheetah3' and the importable is `Cheetah'. A reader who assumed they matched # would look for the wrong dist-info directory. check $P/package-dir test -d "$PKGDIR" check $P/package-init test -s "$PKGDIR/__init__.py" check $P/no-flat-module test ! -f "$SITE/Cheetah.py" check $P/no-cheetah3-package-dir test ! -d "$SITE/Cheetah3" # The modules a caller actually reaches, each named. `Template' and `Parser' are # the engine, `NameMapper' the resolver this port's whole subject is about, # `Compiler' what reads C_VERSION, `CheetahWrapper' what the CLI runs, and # `DirectiveAnalyzer' what `cheetah-analyze' runs. for _m in Template Parser Compiler NameMapper CheetahWrapper DirectiveAnalyzer \ Filters SettingsManager SourceReader Version compat; do check $P/module-$_m test -s "$PKGDIR/$_m.py" done # The sub-packages. `Tests' is what `upstream-ran' below executes; `Templates' # carries the one .tmpl the package ships as data. check $P/subpkg-tests test -d "$PKGDIR/Tests" check $P/subpkg-templates test -d "$PKGDIR/Templates" check $P/subpkg-macros test -d "$PKGDIR/Macros" check $P/subpkg-utils test -d "$PKGDIR/Utils" check $P/subpkg-tools test -d "$PKGDIR/Tools" # The precompiled bytecode, which `import-quiet' below turns on. It matters more # here than in most of these ports: Cheetah's own sources carry a dozen invalid # escape sequences that 3.12 warns about AT COMPILE TIME, so an image that had # to recompile them would say so on stderr at every import. check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-namemapper \ test -s "$PKGDIR/__pycache__/NameMapper.cpython-312.pyc" # THE C ACCELERATOR IS NOT HERE, WHICH IS THE POINT OF THE BBAPPEND. Asserted at # the path the extension would occupy AND across the whole installed file list, # because a release that moved it would defeat the first case alone. # # BRINTOS-WORKAROUND: brintos/linux#73 remove -- read `$PKGLIST' directly when # the redirect stops failing. 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. check $P/no-namemapper-so test ! -e "$PKGDIR/_namemapper.cpython-312.so" 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)" # The C source is not shipped either -- `SetupConfig.py' names it only through # `ext_modules', which the patch empties, so nothing under `Cheetah/c' lands. check_eq $P/pkg-no-c-sources "" \ "$(grep -E '/Cheetah/c/' "$W/pkgf" 2>/dev/null)" # THE THREE EXECUTABLES. Unlike python3-isodate this package DOES install # scripts, and a package can ship a working module and a script whose shebang or # sys.path is wrong -- which is why they are launched as real processes further # down rather than only tested for existence here. check $P/bin-cheetah test -x /usr/bin/cheetah check $P/bin-cheetah-compile test -x /usr/bin/cheetah-compile check $P/bin-cheetah-analyze test -x /usr/bin/cheetah-analyze check_out $P/shebang-cheetah "#!/usr/bin/env python3" head -n 1 /usr/bin/cheetah check_out $P/shebang-cheetah-compile "#!/usr/bin/env python3" \ head -n 1 /usr/bin/cheetah-compile check_out $P/shebang-cheetah-analyze "#!/usr/bin/env python3" \ head -n 1 /usr/bin/cheetah-analyze # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON, A TEMPLATE, OR METADATA. The # cases above name the failures that are expected; this one catches the shape # nobody predicted -- a binary, a .pyd, a stray archive -- without having to # enumerate what it might be. The `.tmpl' and one `.txt' are the package's # declared `data_files' and are allowed by name rather than by pattern. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so each # entry is tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.tmpl) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; /usr/bin/cheetah|/usr/bin/cheetah-compile|/usr/bin/cheetah-analyze) continue ;; */Cheetah/Tools/MondoReportDoc.txt) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python-and-templates "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata. Unlike python3-isodate this package carries TWO # readings of its version -- `Cheetah.Version.Version' in the source and the # dist-info here -- and the probe cross-checks them against each other, which is # what a single reading cannot do. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 3.2.6.post1" cat "$DI/METADATA" check_out $P/dist-info-top-level "Cheetah" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF NOW DECLARES IT. This is the packaging-side # proof that the bbappend took: built WITHOUT the patch the same recipe produced # `Root-Is-Purelib: false' and `Tag: cp312-cp312-linux_x86_64' -- a platform # wheel, tagged with the BUILD host's architecture rather than the target's, # which is a second reason not to want it. Both lines are asserted, because the # tag is what changes if the extension ever comes back. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # The RECORD names every installed file; no `.so' in it is the wheel-side # statement of `pkg-no-so' above. check_eq $P/dist-info-record-no-so "" \ "$(grep -E '\.so,' "$DI/RECORD" 2>/dev/null)" # A LICENCE FILE IS PRESENT HERE, unlike python3-isodate's dist-info -- measured # rather than assumed, because it is the file the recipe's LIC_FILES_CHKSUM is # computed over. check $P/dist-info-license test -s "$DI/LICENSE" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files; this is the one that says # python3-cheetah is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. sed -n '/^Package: python3-cheetah$/,/^$/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" check_out $P/pkg-version "Version: 3.2.6.post1-r0" cat "$W/st" # THE TWO RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. Both # are sub-packages of the ported python3 (BRINTOS-1473, BRINTOS-1476) and were # already on this image, which is why this port waited on nothing. Each is # asserted by name, because "the Depends line is non-empty" would pass with # either missing. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-pickle "python3-pickle" cat "$W/dep" check_out $P/pkg-depends-pprint "python3-pprint" cat "$W/dep" # ...AND python3-unittest IS NOT AMONG THEM, which is the RDEPENDS gap worth # stating: this package ships its own 2125 tests into site-packages and does not # declare what runs them. `upstream-ran' below would go quiet if unittest left # the image, and nothing in the packaging would have warned. check_eq $P/pkg-no-unittest-depend "" \ "$(grep -o 'python3-unittest' "$W/dep" 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If Cheetah 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. `Cheetah.Template' rather than bare # `Cheetah', because the top-level package imports almost nothing and would pass # with the engine missing. check $P/import python3 -c 'import Cheetah.Template' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. A statement about the shipped # bytecode rather than about style: Cheetah's sources carry a dozen invalid # escape sequences ('\d', '\s', '\(' in unraw regex literals) that python3.12 # reports as SyntaxWarning WHEN IT COMPILES THEM. The package ships precompiled # .pyc precisely so that compile never happens at import. A red here means the # shipped bytecode was REJECTED -- a stale hash, a python3 minor bump that moved # the magic, or a missing __pycache__ -- and every import on this image is # paying to recompile 63 modules and printing upstream's warnings while it does. check_eq $P/import-quiet "" \ "$(python3 -c 'import Cheetah.Template' 2>&1)" # ---------------------------------------------------------------- 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" 600 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 `ph_attr' # distinguishable from `ph_attr_then_key'. 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 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 # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart, and `probe-count' is # what catches a key skipped in the middle. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "100" "$(pv emitted_count)" # The module answered out of the file this image installed, not from a stray # copy on sys.path -- which is what makes the payload cases above and the # behavioural cases below statements about the SAME Cheetah. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # ------------------------------------------------------------- the identity --- check_eq $P/is-package "yes" "$(pv is_package)" # TWO READINGS OF THE VERSION, cross-checked against each other. `Version.py' is # the source of truth the package quotes to callers; the dist-info is what the # packaging wrote. They agree here, and a release that moved one without the # other would red on exactly one of these two lines. check_eq $P/version "3.2.6.post1" "$(pv version)" check_eq $P/dist-version "3.2.6.post1" "$(pv dist_version)" check_eq $P/version-tuple "3,2,6,post,1" "$(pv version_tuple)" check_eq $P/min-compatible "3.0.0a1" "$(pv min_compatible)" # `Cheetah.Version' IS A STRING ON THE PACKAGE, not the module of that name -- # `__init__.py' re-exports it and shadows the submodule. `from Cheetah import # Version' therefore gives text where a reader expects a module, which is a trap # worth pinning rather than a curiosity. check_eq $P/version-attr-is-str "str" "$(pv version_attr_type)" # The DISTRIBUTION is `Cheetah3'; asking importlib.metadata for `Cheetah' raises. # Both halves, because the name mismatch is the thing a caller gets wrong. check_eq $P/dist-name-is-cheetah3 "PackageNotFoundError" \ "$(pv dist_version_wrong_name)" # ------------------------------------------------- THE ACCELERATOR, DECLINED --- # THE CENTRAL GROUP OF THIS SUITE. `C_VERSION' is False because the bbappend # left `ext_modules' empty for class-target, so `Cheetah._namemapper' was never # built and NameMapper.py's try/except kept the pure-Python definitions it made # first. `cver-ext-import' is the direct statement of the absence, and the two # `__module__' cases say which implementation every other case in this file is # actually measuring. check_eq $P/cver-off "no" "$(pv cver_c_version)" check_eq $P/cver-ext-absent "ModuleNotFoundError" "$(pv cver_ext_import)" check_eq $P/cver-valuefor-name-is-python "Cheetah.NameMapper" \ "$(pv cver_valueForName_module)" check_eq $P/cver-notfound-is-python "Cheetah.NameMapper" \ "$(pv cver_notfound_module)" # NotFound is a LookupError either way, so a caller catching it by name is # unaffected by which arm answered. Stated because it is the reason the # fallback is a fallback rather than a different library. check_eq $P/cver-notfound-is-lookuperror "yes" \ "$(pv cver_notfound_is_lookuperror)" # `valueFromFrame' is still EXPORTED by the pure-Python module -- the API surface # does not shrink, only the compiler stops calling it. Asserted so `useStackFrames # off' is not misread as "the frame functions are gone". check_eq $P/cver-frame-api-still-there "yes" "$(pv cver_has_valueFromFrame)" # ------------------------------------- WHAT DECLINING IT DOES TO THE COMPILER --- # `Compiler.py' warns and forces `useStackFrames' off when C_VERSION is False. # The warning is what a user SEES -- it appears on stderr of every # `cheetah-compile' run, which `cli-compile-warns' below measures on the real # CLI -- and the setting is what actually changes the generated code. check_eq $P/stackframes-warned-once "1" "$(pv stackframes_warning_count)" check_eq $P/stackframes-warning-names-c "yes" "$(pv stackframes_warning_names_c)" check_eq $P/stackframes-warning-names-option "yes" \ "$(pv stackframes_warning_names_option)" check_eq $P/stackframes-off "yes" "$(pv stackframes_setting_off)" # The emitted call form, which is the whole of the difference. Both `VFFSL' and # `VFSL' are bound in every generated module's preamble, so their presence says # nothing; what changes is which the placeholder call sites use. check_eq $P/stackframes-src-prepends-locals "yes" \ "$(pv stackframes_src_prepends_locals)" check_eq $P/stackframes-src-no-vffsl-callsite "yes" \ "$(pv stackframes_src_no_vffsl_callsite)" # `Template.compile(returnAClass=False)' hands back BYTES on this release, not # str -- a seam a caller writing the generated module to a text file trips over, # and one this suite's own probe had to handle. check_eq $P/generated-source-is-bytes "bytes" "$(pv stackframes_src_type)" # ----------------------------------------------------------- the placeholder --- # `$name' resolution, which is the whole of what NameMapper does and therefore # the half of Cheetah the accelerator decision touches. Every one of these # answered identically with the extension built (x86-64 control, 2026-09-02). check_eq $P/ph-simple "Hello first!" "$(pv ph_simple)" check_eq $P/ph-braced "firstly" "$(pv ph_braced)" # The escape is a backslash, and the rendered output carries neither it nor a # substitution. check_eq $P/ph-escaped "costs \$who" "$(pv ph_escaped)" # A `$' followed by something that cannot start a name is left alone WITHOUT an # escape, which is why a shell- or currency-flavoured template usually renders # unharmed. check_eq $P/ph-bare-dollar "100\$ and first" "$(pv ph_bare_dollar)" check_eq $P/ph-attr "Ada" "$(pv ph_attr)" check_eq $P/ph-class-attr "class attribute" "$(pv ph_class_attr)" # AUTOCALLING. A placeholder naming a method CALLS it -- no parentheses -- which # is the single most surprising thing about reading a Cheetah template. The # explicit-call form is asserted beside it so the two are visibly one answer. check_eq $P/ph-method-autocall "hi" "$(pv ph_method_autocall)" check_eq $P/ph-method-called "hi" "$(pv ph_method_called)" check_eq $P/ph-method-args "15" "$(pv ph_method_args)" check_eq $P/ph-method-kwargs "6" "$(pv ph_method_kwargs)" check_eq $P/ph-func-call "14" "$(pv ph_func_call)" # A DICT IS WALKED WITH A DOT. `$d.k' is not attribute access -- the mapping is # tried first -- and the bracket form means the same thing. A caller who knows # only Python reads the first as a typo. check_eq $P/ph-dict-dot "dictval" "$(pv ph_dict_dot)" check_eq $P/ph-dict-brackets "dictval" "$(pv ph_dict_brackets)" check_eq $P/ph-dict-nested "nested" "$(pv ph_dict_nested)" check_eq $P/ph-list-index "b" "$(pv ph_list_index)" check_eq $P/ph-attr-then-index "z" "$(pv ph_attr_then_index)" check_eq $P/ph-attr-then-key "buried" "$(pv ph_attr_then_key)" # SEARCHLIST ORDER IS FIRST-WINS, and a name absent from the head namespace # falls through to the next. Both directions, because either alone would pass # with the list reversed. check_eq $P/ph-ns-first-wins "first" "$(pv ph_ns_first_wins)" check_eq $P/ph-ns-falls-through "two" "$(pv ph_ns_falls_through)" check_eq $P/ph-unicode "é中" "$(pv ph_unicode)" # NO ESCAPING BY DEFAULT. Cheetah's default filter passes text through, so a # template rendering user data into HTML is not protected by the engine. Stated # because the opposite is the default in most engines a reader will have used. check_eq $P/ph-no-html-escape "&" "$(pv ph_no_html_escape)" # ------------------------------------- the dict-method collision, on both arms --- # See the header. `items' resolves to the local namespace dict's bound method # rather than to the searchList value, and the placeholder machinery autocalls # it. `collide-not-a-method' is the control: the SAME list under a name that is # not part of the mapping API renders as itself. check_eq $P/collide-dict-method "dict_items" "$(pv collide_dict_method)" check_eq $P/collide-index-typeerror "TypeError" "$(pv collide_dict_method_index)" check_eq $P/collide-for-runtimeerror "RuntimeError" "$(pv collide_dict_method_for)" check_eq $P/collide-not-a-method "['a', 'b', 'c']" "$(pv collide_not_a_method)" # ------------------------------------------------------------- the directives --- # The control flow, and the whitespace it does or does not leave behind. # Newlines are spelled `\n' by the probe and by these expectations alike; a # comparison that stripped them would never see `#slurp' failing to slurp. check_eq $P/dir-if-true "yes\\n" "$(pv dir_if_true)" check_eq $P/dir-if-false "no\\n" "$(pv dir_if_false)" check_eq $P/dir-unless "ran\\n" "$(pv dir_unless)" check_eq $P/dir-for "a.\\nb.\\nc.\\n" "$(pv dir_for)" check_eq $P/dir-for-empty "" "$(pv dir_for_empty)" check_eq $P/dir-for-enumerate "0=a \\n1=b \\n2=c \\n" "$(pv dir_for_enumerate)" check_eq $P/dir-while "012" "$(pv dir_while)" check_eq $P/dir-set "42" "$(pv dir_set)" # `#set' writes into the template's own namespace and does NOT write back into # the caller's dict. Both halves are in one value -- what the template rendered, # and what the caller's namespace still holds afterwards. check_eq $P/dir-set-does-not-mutate "local|first" \ "$(pv dir_set_does_not_mutate_ns)" check_eq $P/dir-echo "42" "$(pv dir_echo)" check_eq $P/dir-silent "-" "$(pv dir_silent)" # `#slurp' eats the newline after it; `##' is a line comment and eats its line; # `#* *#' is the block form. All three are about whitespace, which is what these # exact-string comparisons are uniquely good at pinning. check_eq $P/dir-slurp "ab" "$(pv dir_slurp)" check_eq $P/dir-comment-line "a\\nb\\n" "$(pv dir_comment_line)" check_eq $P/dir-comment-block "a\\nb\\n" "$(pv dir_comment_block)" check_eq $P/dir-raw "\$who and #if x\\n" "$(pv dir_raw)" check_eq $P/dir-def "Hi Bob!" "$(pv dir_def)" check_eq $P/dir-block "default text\\n" "$(pv dir_block)" check_eq $P/dir-attr "const" "$(pv dir_attr)" # A `#' NOT FOLLOWED BY A DIRECTIVE NAME IS ORDINARY TEXT, which is what lets a # Cheetah template generate shell, Python or a Makefile without escaping every # comment in it. check_eq $P/dir-hash-passes-through "# not a directive\\nx\\n" \ "$(pv dir_pass_through_hash)" # ------------------------------------------------------- inheritance and files --- # `#extends' is CLASS inheritance, and this is the group that says so end to end: # the parent rendered alone, then a child that replaces ONE of the parent's two # blocks. The parent's `title' surviving into the child is what distinguishes # inheritance from the child simply being a different template. check_eq $P/inh-parent "parent title\\nparent body\\n" "$(pv inh_parent_alone)" check_eq $P/inh-child-overrides-one-block "parent title\\nchild body\\n" \ "$(pv inh_child_overrides)" # `#include' is the other composition, and it is a RUNTIME read of another file: # the included template is parsed and rendered against the same searchList, so # `$who' resolves inside it. `#include raw' is the same read without the parse, # which is why the `$who' comes back untouched. check_eq $P/inc-file "before\\nincluded first\\nafter\\n" "$(pv inc_file)" check_eq $P/inc-raw-file "included \$who\\n" "$(pv inc_raw_file)" # --------------------------------------------------------- the failure paths --- # A placeholder naming nothing raises at RENDER time and not at compile time, so # a template with a typo in it compiles clean and fails in production. That # asymmetry is the shape of the failure and it is what these two cases say. check_eq $P/fail-missing-name "NotFound" "$(pv fail_missing_name)" check_eq $P/fail-missing-compiles-clean "returned" \ "$(pv fail_missing_at_compile)" check_eq $P/fail-missing-attr "NotFound" "$(pv fail_missing_attr)" check_eq $P/fail-missing-key "NotFound" "$(pv fail_missing_key)" # A malformed directive DOES fail at compile time, and with a different # exception -- so the two halves of "wrong template" are distinguishable. check_eq $P/fail-bad-syntax "ParseError" "$(pv fail_bad_syntax)" check_eq $P/fail-unclosed "ParseError" "$(pv fail_unclosed_directive)" # `getVar' with a default is the sanctioned way to ask without raising, and # `varExists' the way to ask whether it would. check_eq $P/api-getvar "first" "$(pv api_getvar)" check_eq $P/api-getvar-default "1234" "$(pv api_getvar_default)" check_eq $P/api-varexists-true "yes" "$(pv api_varExists_true)" check_eq $P/api-varexists-false "no" "$(pv api_varExists_false)" # THE ONE PLACE THIS IMAGE ANSWERS DIFFERENTLY IN KIND, and the reason four of # upstream's tests error below. `getVar("$")' strips the sigil and asks for the # empty key: the C mapper raises ValueError, the pure-Python one raises # NameMapper.NotFound. A caller with `except ValueError' around it catches # nothing here -- which the case beside it states as the property it is. check_eq $P/api-getvar-dollar-raises-notfound "NotFound" "$(pv api_getvar_dollar)" check_eq $P/api-notfound-is-not-valueerror "yes" \ "$(pv api_notfound_is_not_valueerror)" # ------------------------------------------------- compile, write, import, run --- # The generated module is real Python written to a file, imported by name and # rendered -- the path `cheetah-compile' takes, and the reason this package is a # code GENERATOR and not only an engine. The assertion that closes it is that # the imported module produces the SAME output as rendering the source directly, # which neither half proves alone. check_eq $P/gen-module-is-python "ok" "$(pv gen_module_is_python)" check_eq $P/gen-imported-render "generated: first and dictval\\n" \ "$(pv gen_imported_render)" check_eq $P/gen-matches-direct "yes" "$(pv gen_matches_direct)" # ------------------------------------------------------ NameMapper, directly --- # The resolver under the placeholder syntax, exercised through its own API, so a # red in the render cases above can be told apart from a red in the resolver. check_eq $P/nm-valuefor-key "v" "$(pv nm_valueForKey)" check_eq $P/nm-valuefor-name-dotted "buried" "$(pv nm_valueForName_dotted)" check_eq $P/nm-valuefor-name-attr "Ada" "$(pv nm_valueForName_attr)" check_eq $P/nm-value-from-searchlist "first" "$(pv nm_valueFromSearchList)" check_eq $P/nm-haskey-true "yes" "$(pv nm_hasKey_true)" check_eq $P/nm-haskey-false "no" "$(pv nm_hasKey_false)" check_eq $P/nm-hasname-true "yes" "$(pv nm_hasName_true)" check_eq $P/nm-hasname-false "no" "$(pv nm_hasName_false)" check_eq $P/nm-notfound "NotFound" "$(pv nm_notfound)" # executeCallables is OFF at this API's front door and ON in a template # placeholder. Both are asserted, because that difference is the reason a # template and a direct call disagree about the same name. check_eq $P/nm-no-autocall-by-default "yes" "$(pv nm_no_autocall_by_default)" check_eq $P/nm-autocall-when-asked "hi" "$(pv nm_autocall_when_asked)" # ------------------------------------------------------------- the analyzer --- # `DirectiveAnalyzer' is what `cheetah-analyze' runs, and the only part of this # package that reads a template without compiling it. check_eq $P/analyze-for "1" "$(pv analyze_for)" check_eq $P/analyze-if "1" "$(pv analyze_if)" check_eq $P/analyze-set "1" "$(pv analyze_set)" # ------------------------------------------------- THE CLI, AS REAL PROCESSES --- # Three launches of the shipped scripts through their own shebang. This is not # foldable into the probe: a package can ship a working module and a script # whose shebang or sys.path is wrong, and only a real exec says which. It is # also the whole of the CLI coverage in this port, since upstream's own 45 # CheetahWrapper tests are excluded from the run below for being subprocess # bound. CW=$W/cli mkdir -p "$CW" printf 'Hello $who from $d.k\n' > "$CW/t.tmpl" printf 'plain text, no placeholders\n' > "$CW/s.tmpl" printf '#for $i in $seq\n#if $i\n$i\n#end if\n#end for\n#set $y = 1\n' \ > "$CW/a.tmpl" # `cheetah-compile' writes the generated MODULE beside the template. Its stdout # names the transformation, and the generated file must carry the class the # template's basename implies -- which is what an importer will look for. check_out $P/cli-compile "Compiling t.tmpl -> t.py" \ sh -c "cd '$CW' && /usr/bin/cheetah-compile t.tmpl 2>/dev/null" check $P/cli-compile-wrote test -s "$CW/t.py" check_out $P/cli-compile-class "class t" cat "$CW/t.py" # ...AND IT WARNS ON STDERR WHILE DOING IT, every time, because C_VERSION is # False. This is the accelerator decision as a USER meets it: not a silent # fallback but a paragraph on stderr of every compile. It is asserted rather # than filtered so that a later build which restored the extension would red # here and be noticed. check_out $P/cli-compile-warns "C version of NameMapper" \ sh -c "cd '$CW' && /usr/bin/cheetah-compile s.tmpl 2>&1 >/dev/null" # `cheetah fill' is the other half of the CLI: render to an output file rather # than generate a module. A template with no placeholders is used deliberately, # so this case measures the SCRIPT working and not the engine, which the probe # has already covered a hundred ways. check_out $P/cli-fill "Filling s.tmpl -> s.txt" \ sh -c "cd '$CW' && /usr/bin/cheetah fill --oext txt s.tmpl 2>/dev/null" check_out $P/cli-fill-output "plain text, no placeholders" cat "$CW/s.txt" # `cheetah-analyze' reads a template without compiling it and reports the # directive census -- the same numbers the probe read out of `DirectiveAnalyzer' # in process, now through the shipped script. `end' is 2 because `#end if' and # `#end for' are counted as directives of their own. check_out $P/cli-analyze "{'end': 2, 'for': 1, 'if': 1, 'set': 1}" \ sh -c "cd '$CW' && /usr/bin/cheetah-analyze -f a.tmpl 2>/dev/null" # ------------------------------------------ UPSTREAM'S OWN TEST SUITE, RUN --- # See the header. 2125 of the author's tests over the thirteen test modules this # package ships into site-packages, in their own interpreter start with their # own bound -- so an overrun costs these ten cases and not the ninety above. if [ ! -f "$UPROBE" ]; then bst_notok $P/upstream-probe-present "$UPROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/upstream-probe-present bst_probe $P/upstream-run "$W/up" 900 python3 "$UPROBE" UOUT="$W/up" uv() { grep "^$1=" "$UOUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } check_eq $P/upstream-complete "yes" "$(uv probe_done)" check_eq $P/upstream-count "11" "$(uv emitted_count)" # THE COLLECTION IS ASSERTED SEPARATELY FROM THE RESULT. A run that collected # nothing reports zero failures and looks green, which is exactly what `python3 # -m unittest Cheetah.Tests' does here -- upstream's aggregator is a module-level # `suites' list that unittest's discovery does not find. These two are what tell # "2125 tests ran" apart from "no test ran". check_eq $P/upstream-collected "2125" "$(uv upstream_collected)" check_eq $P/upstream-modules "13" "$(uv upstream_modules)" check_eq $P/upstream-ran "2125" "$(uv upstream_ran)" check_eq $P/upstream-failures "0" "$(uv upstream_failures)" # FIVE ERRORS, EXPECTED AND NAMED -- FOUR FROM THE ACCELERATOR DECISION, ONE # FROM THE STATIC INTERPRETER. Measured on this arm by the batch-2084 verify, # 2026-09-02; the first four were also measured on the x86-64 control, and the # fifth is the one thing the control cannot show. # # The four are `$getVar("$")' asserting the C accelerator's ValueError, in the # four EOL variants of the GetVar class -- the accelerator decision showing # through upstream's own tests. # # The fifth, `ImportHooksTest.test_import_builtin', is an upstream defect that # this arm exposes and the control does not, and it is not on any path this port # changed. The test imports the FIRST entry of `sys.builtin_module_names' that # is not already in `sys.modules'. This image has no `lib-dynload' at all -- # every extension module is linked into the interpreter -- so that list here # holds what the control keeps in .so files, and the two arms therefore pick a # different module. The one picked here is imported while `Cheetah.ImportHooks' # is installed and its initialisation reaches a relative import, which upstream # `ImportManager.importHook' mishandles: it ignores the `level' argument, so it # is handed the empty name, `nameSplit("")' returns `[]', its `for i in # range(len(nmparts))' never binds `i', and the `else' clause's `i = i + 1' # raises `UnboundLocalError'. The templates half of the same test module, # `test_ImportHooks', passes -- so the hook itself works for what this package # is for. # # Named rather than counted: a bare `5' would keep passing if one of these # turned green and something else broke. check_eq $P/upstream-errors "5" "$(uv upstream_errors)" check_eq $P/upstream-error-ids \ "Cheetah.Tests.ImportHooks.ImportHooksTest.test_import_builtin,Cheetah.Tests.SyntaxAndOutput.GetVar.test6,Cheetah.Tests.SyntaxAndOutput.GetVar_DiffBaseClass.test6,Cheetah.Tests.SyntaxAndOutput.GetVar_MacEOL.test6,Cheetah.Tests.SyntaxAndOutput.GetVar_Win32EOL.test6" \ "$(uv upstream_error_ids)" check_eq $P/upstream-failure-ids "" "$(uv upstream_failure_ids)" # ...AND THE EXCEPTIONS THEY DIED OF, which is the one line that says WHY they # error rather than merely that they do. `Cheetah.NameMapper.NotFound' is the # pure-Python answer for the four; a build carrying the extension raises # ValueError and that group goes green. `UnboundLocalError' is the fifth, and # names a different fault from the four rather than hiding inside their count. check_eq $P/upstream-error-class "Cheetah.NameMapper.NotFound,UnboundLocalError" \ "$(uv upstream_error_classes)" # python3-markdown is not on this image, and `Cheetah.Tests.Filters' says so on # stdout at import. Asserted so the line in the transcript is a stated fact # rather than something a reader has to rule out. check_eq $P/upstream-markdown-absent "absent" "$(uv upstream_markdown)" rm -rf "$W"