. "$BST_LIB" # BST-TIMEOUT: 900 # THREE python3 starts, and they are the whole cost. A # # python3 start on this arm has measured at tens of seconds # # and everything this suite asks python3 to DO is regex # # matching and LALR table construction over a nine-token, # # ten-production grammar -- microseconds beside the start. # # # # The three are: a bare `import ply', a second import whose # # STDERR is the assertion, and the probe, which takes ONE # # start for eighty-odd measurements. Everything else here is # # a shell builtin over the filesystem or a grep of the # # probe's transcript. # # # # 900 is 300s per start -- five times the ~60s a python3 # # start has measured at on this arm. 99-python3-smartypants # # budgets 1800 for ten starts (180s each); this is looser # # per start because the probe's start is the one that also # # builds the LALR tables, and looser is the cheap direction: # # a suite-timeout red costs the whole batch a round. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. 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-ply 3.11 (poky/meta recipes-devtools/python/python3-ply_3.11.bb, # `inherit pypi setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS. # ============================================================================ # # PLY is lex and yacc for Python, written by David Beazley. `ply.lex' builds a # tokeniser out of regular expressions declared as module attributes -- the # NAME of the attribute is the token type, and the docstring of a function rule # is its pattern. `ply.yacc' builds an LALR(1) parser out of grammar rules # written as the DOCSTRINGS of `p_*' functions, whose bodies are the semantic # actions. # # Both halves are exercised here as a caller would use them: one arithmetic # language, tokenised token by token and then parsed to values. # # ============================================================================ # THE ONE THING THAT BREAKS THIS PACKAGE ON THIS IMAGE, AND WHAT THE SUITE DOES # ABOUT IT. # ============================================================================ # # `yacc()' GENERATES SOURCE AND WRITES IT TO DISK. It emits a table module # (`parsetab.py' by default) and, with `debug' on -- which is the DEFAULT -- a # human-readable `parser.out'. Where it writes them is the directory of the # module that DEFINES the grammar, unless the caller passes `outputdir'. # # On this image that directory is /usr/lib/brintos-tests/data -- $BST_DATA -- on # the READ-ONLY virtiofs root. So the probe is handed a writable working # directory and passes it as `outputdir' for every write; `outputdir-*' records # both the path it would have used and the one it did, so this is asserted # rather than merely arranged. # # THERE IS A SECOND CONDITION ON REUSE AND IT IS THE ONE NOBODY EXPECTS. # `yacc()' looks for an existing table by IMPORTING it -- `read_table' is an # `__import__' -- so a table written somewhere that is not on `sys.path' is # written on every single run and read on none. `outputdir' buys the WRITE; # `sys.path' buys the READ. `workdir-on-path' states the second half, and the # `reuse-*' group is what would be red without it. # # ============================================================================ # THE REUSE CASES ASSERT AN ABSENCE, SO THEY CARRY THEIR OWN CONTROL. # ============================================================================ # # `reuse-mtime', `reuse-size' and `reuse-debugfile' assert that a second build # of the SAME grammar leaves the generated files alone. An assertion that # nothing happened is passed trivially by a build that never looked at the file, # so `stale-regenerated' is its control: the recorded grammar signature is # corrupted, the identical grammar is built again, and the table MUST be # rewritten. `stale-signature-restored' then says what came back is the right # table rather than merely a newer file. # # The mtimes are BACKDATED before the second build rather than merely read. # Filesystem mtime granularity on this image's virtiofs root is not knowable # from a shell, and two writes inside one tick would otherwise compare equal and # pass these cases for exactly the wrong reason. # # ============================================================================ # WHY IT PORTS: PURE PYTHON, AND ITS TWO DECLARED DEPENDENCIES ARE UNEXERCISED. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), so a module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. ply ships no C at all: `setup.py' declares # `packages=['ply']' with no `ext_modules', no `scripts' and no entry points. # # The recipe's RDEPENDS names `python3-netclient' and `python3-shell'. MEASURED, # and neither is exercised: `rdepends-pulled' imports all five submodules and # reports which of the ten stdlib names those two sub-packages own were dragged # in as a DELTA against the interpreter's state before ply was touched. The # answer is none. `ply/ygen.py' does import `shutil', which is python3-core's # and not python3-shell's -- `pulled-shutil' is what says so. # # That is an over-declaration in oe-core's recipe, not a defect of the port and # not one of ours to file (`no-public-upstream-reports'); it is asserted so that # a reader who found the RDEPENDS line and expected a dependency chain is told # what is actually there. # # It is a PACKAGE, NOT A MODULE -- `packages=['ply']' puts a DIRECTORY in # site-packages, the opposite shape from python3-smartypants's single file. # `is-package' and `pkg-dir' assert it directly, because a release that # flattened it would still import and still pass every behavioural case. # # ============================================================================ # TWO UPSTREAM BEHAVIOURS RECORDED AS WHAT THEY DO. # ============================================================================ # # A reader who worked either out from the documentation would call it a defect # of the port. Neither is this platform's; both reproduce on x86-64 CPython # against the same sources. # # FIRST, A SYNTAX ERROR DOES NOT MAKE parse() RETURN None. It calls `p_error' # -- which is the contract, and `synerr-raises' is the case for it -- and then # enters yacc's ERROR RECOVERY, discarding tokens until the parse can continue. # Over `2 + + 3' that leaves the trailing `3' to be reduced, so parse() returns # 3 and not None. A caller that tested `if result is None' to detect a syntax # error would find no error had occurred. `synerr-result' pins it. At END OF # INPUT there is nothing left to recover with, so `2 +' DOES return None and the # hook is called with `None' rather than a token -- a different branch, and the # one a caller who wrote `p.value' unguarded crashes in. `synerr-eof-*' is that # pair. # # SECOND, lex AND yacc ARE NOT SYMMETRIC ABOUT WRITING TABLES. `yacc()' writes # one on every build; `lex()' writes one only when `optimize' is set. # `lextab-default' asserts the absence and `lextab-opt' the presence, because a # caller who assumed the yacc behaviour and made the working directory read-only # would find lex silent and yacc broken, and would look in the wrong place. # # ============================================================================ # WHAT IS NOT HERE. # ============================================================================ # # NO CLI. Unlike python3-smartypants this package installs no executable at all # -- `setup.py' declares neither `scripts' nor `console_scripts' -- so there is # no shebang to assert and no `env' question to ask. `pkg-no-bin' is the case # that would notice a release that grew one. # # NO ptest. The recipe does not `inherit ptest', so no -ptest package could be # produced whatever DISTRO_FEATURES said. Upstream ships a `test/' directory in # the sdist that the wheel does not install; what it covers is reproduced case # by case in the probe instead, against one grammar rather than forty. P=python3-ply # THE CANARY, which asserts nothing about python3-ply. # # 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 PKG=$SITE/ply DI=$SITE/ply-3.11.dist-info PKGLIST=/var/lib/dpkg/info/python3-ply.list PROBE=$BST_DATA/python3-ply-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-ply.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import ply' 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 on sys.path is distinguishable from one that # imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A FILE. See the header: `packages=['ply']' is the opposite # shape from python3-smartypants's `py_modules', and this is the case that fails # a release which flattened it -- loudly, here, rather than nowhere. check $P/pkg-dir test -d "$PKG" check $P/pkg-not-file test ! -f "$SITE/ply.py" check $P/mod-init test -s "$PKG/__init__.py" # THE TWO HALVES OF THE DELIVERABLE, as files. Everything below is about what # they do; these say they arrived. check $P/mod-lex test -s "$PKG/lex.py" check $P/mod-yacc test -s "$PKG/yacc.py" # ...and the three that are easiest to lose in a repackaging: `cpp' and # `ctokens' are a C preprocessor built ON ply, and `ygen' is its table # regenerator. None is imported by lex or yacc, so nothing else in this file # would notice their absence. check $P/mod-cpp test -s "$PKG/cpp.py" check $P/mod-ctokens test -s "$PKG/ctokens.py" check $P/mod-ygen test -s "$PKG/ygen.py" # The precompiled bytecode, which is what `import-quiet' below rests on. check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-lex test -s "$PKG/__pycache__/lex.cpython-312.pyc" check $P/pycache-yacc test -s "$PKG/__pycache__/yacc.cpython-312.pyc" # 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 -- at install rather than at `import'. # # 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)" # NO EXECUTABLE EITHER, which is this package's difference from every other # python3 port in this image. See the header: there is no `scripts', no # `console_scripts' and therefore no shebang and no `env' dependency. This is # the case that would notice a release that grew one. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The two cases # above name the failures that are expected; this one catches the shape nobody # predicted -- a data file, a .pyd, a stray archive -- 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 ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata. The package carries its own `__version__', so the # dist-info is the SECOND of two independent statements of the version rather # than the only one, and `version-agrees' below is what compares them. 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.11" cat "$DI/METADATA" check_out $P/dist-info-name "Name: ply" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import ply' has to match, and a wheel that renamed the package would change # it here first. check_out $P/dist-info-top-level "ply" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # ------------------------------------------------------------- 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-ply 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-ply$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. See the # header: two of the three are over-declarations that nothing in the package # exercises, and `rdepends-pulled' below is the measurement that says so. These # cases pin what was DECLARED, so the two statements can be compared by a reader # rather than conflated. 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-netclient "python3-netclient" cat "$W/dep" check_out $P/pkg-depends-shell "python3-shell" cat "$W/dep" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If ply 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 ply, ply.lex, ply.yacc' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode, not a style assertion: the package ships precompiled .pyc # files and OE writes them with checked-hash invalidation, so the source is # hashed and never recompiled. A red here means the shipped .pyc was REJECTED -- # a stale hash, a python3 minor bump that changed the magic, or a missing # __pycache__ -- and the modules are being compiled from source on every import. # # It also covers a second thing worth knowing: `ply/cpp.py' opens with # `from __future__ import generators', which is a no-op in Python 3 and warns in # none of them, and `ply/yacc.py' imports `warnings' without configuring it. A # red here on a version bump most likely means one of those started to speak. check_eq $P/import-quiet "" \ "$(python3 -c 'import ply, ply.lex, ply.yacc, ply.cpp, ply.ctokens, ply.ygen' 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, and the WORKING DIRECTORY # is its argument -- see the header: every file yacc generates is written there, # because the alternative is the read-only root. 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. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" "$W" 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 anchor keeps `tok1' distinguishable # from `tok11', which is the pair this suite most needs to keep apart. 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. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "84" "$(pv emitted_count)" # The probe's STDERR, which is empty on a clean run. yacc's diagnostics -- # shift/reduce conflicts, unused tokens, an undefined precedence -- go to its # error log, and the probe silences that deliberately so the cases below measure # the parser rather than its warnings. What is left on this stream is a Python # traceback, so a red here is the one that explains a whole red group at once. check_eq $P/probe-stderr "" "$(cat "$OUT.err" 2>/dev/null)" # ------------------------------------------------------------- the identity --- # The package answered out of the directory this image installed, not from a # stray copy on sys.path. This is what makes the payload cases above and the # behaviour cases below statements about the SAME ply. check_eq $P/package-answered "$PKG/__init__.py" "$(pv package_file)" # ...and it is a PACKAGE. See the header. check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION, FROM BOTH PLACES, AND THEN AGAINST EACH OTHER. The package's own # `__version__' and the installed dist-info read through importlib.metadata are # independent statements; `version-agrees' is the one that catches a package # that landed beside a dist-info describing some other release, which either # case alone would pass. check_eq $P/version-attr "3.11" "$(pv version_attr)" check_eq $P/dist-version "3.11" "$(pv dist_version)" check_eq $P/version-agrees "$(pv version_attr)" "$(pv dist_version)" # The two submodules carry their own `__version__' as well, and all three are # expected to agree; upstream bumps them by hand, so a release that missed one # is a real possibility rather than a hypothetical. check_eq $P/lex-version "3.11" "$(pv lex_version)" check_eq $P/yacc-version "3.11" "$(pv yacc_version)" # THE TABLE-FORMAT VERSION, WHICH IS NOT THE PACKAGE VERSION and lags it by # design: 3.10 in a 3.11 release. Both lex and yacc refuse a generated table # whose recorded `_tabversion' differs from this string and regenerate instead, # so it is the constant the whole `reuse-*' group below rests on. check_eq $P/lex-tabversion "3.10" "$(pv lex_tabversion)" check_eq $P/yacc-tabversion "3.10" "$(pv yacc_tabversion)" # `__all__' is what `from ply import *' delivers, and it names two of the five # submodules -- so it is also the statement that `cpp', `ctokens' and `ygen' are # reachable but not advertised. check_eq $P/all-attr "lex,yacc" "$(pv all_attr)" # EVERY SUBMODULE IMPORTS. The file cases above say the sources shipped; these # say each one is importable, which for `cpp' -- whose first line is a # `from __future__' import -- is not the same statement. for _m in lex yacc cpp ctokens ygen; do check_eq $P/submod-$_m "yes" "$(pv submod_$_m)" done # THE DECLARED DEPENDENCIES ARE NOT EXERCISED. See the header: importing all # five submodules pulls in NONE of the ten stdlib names python3-netclient and # python3-shell own. This is a delta against the interpreter's state before ply # was touched, not a membership test, because the probe's own use of # importlib.metadata drags `base64' and `urllib' in and a membership test would # read those as ply's. check_eq $P/rdepends-pulled "none" "$(pv rdepends_pulled)" # ...and the three names that MUST be in that delta, so the case above cannot # pass by having measured nothing at all. `shutil' is the interesting one: it is # `ply/ygen.py's only import and it is python3-CORE's, which is what makes the # python3-shell line above readable as an over-declaration. check_eq $P/pulled-ply "yes" "$(pv pulled_ply)" check_eq $P/pulled-re "yes" "$(pv pulled_re)" check_eq $P/pulled-shutil "yes" "$(pv pulled_shutil)" # The delta is not asserted against a literal -- it carries `bz2', `lzma' and # `zlib', which shutil imports under try/except, so its membership is a # statement about which optional extension modules this image's python3 has # rather than about ply. Its SIZE is asserted, which is what says a measurement # happened at all. check_eq $P/pulled-nonempty "yes" "$(pv pulled_enough)" # lex's own imports, by attribute rather than by reading the source: a release # that grew a dependency is named here rather than discovered at the first # import on a smaller image. check_eq $P/lex-imports "copy,inspect,os,re,sys,types" "$(pv lex_imports)" # --------------------------------------------------------------- the lexer --- # TOKEN BY TOKEN, over an input carrying every one of the nine token types and # a line break, so `lineno' is a measurement rather than the constant 1. Each # token is asserted on ALL FOUR of its public attributes -- type, value, line # and position -- because a lexer that got `value' right and `lexpos' wrong is # broken for every caller that reports a position, and no value-only case would # see it. # # `lexpos' is an offset into the WHOLE input, not into the line, which is the # detail the two second-line tokens (28, 30, 32) pin. check_eq $P/tok-count "16" "$(pv tok_count)" check_eq $P/tok00 "NAME|a|1|0" "$(pv tok0)" check_eq $P/tok01 "EQUALS|=|1|2" "$(pv tok1)" # NUMBER is the one token type with a FUNCTION rule, and its action replaces the # matched text with an int -- so this case is also the statement that the action # ran. Every arithmetic result below depends on it. check_eq $P/tok02 "NUMBER|12|1|4" "$(pv tok2)" check_eq $P/tok03 "PLUS|+|1|7" "$(pv tok3)" check_eq $P/tok04 "NUMBER|3|1|9" "$(pv tok4)" check_eq $P/tok05 "TIMES|*|1|11" "$(pv tok5)" check_eq $P/tok06 "LPAREN|(|1|13" "$(pv tok6)" check_eq $P/tok07 "NUMBER|40|1|15" "$(pv tok7)" check_eq $P/tok08 "MINUS|-|1|18" "$(pv tok8)" check_eq $P/tok09 "NUMBER|4|1|20" "$(pv tok9)" check_eq $P/tok10 "RPAREN|)|1|22" "$(pv tok10)" check_eq $P/tok11 "DIVIDE|/|1|24" "$(pv tok11)" check_eq $P/tok12 "NUMBER|2|1|26" "$(pv tok12)" # THE LINE COUNTER ADVANCED. `t_newline' is a rule that returns nothing -- it # consumes the newline and bumps `lexer.lineno' -- so these three are the only # cases that say it ran. A lexer whose `t_newline' silently did not fire would # pass every case above and every case below. check_eq $P/tok13 "NAME|b|2|28" "$(pv tok13)" check_eq $P/tok14 "EQUALS|=|2|30" "$(pv tok14)" check_eq $P/tok15 "NAME|a|2|32" "$(pv tok15)" # THE STREAM ENDS. `token()' returning None at end of input is what every # caller's loop terminates on; a lexer that returned a sentinel token instead # would hang each of them, and this suite would time out rather than red. check_eq $P/lex-eof "None" "$(pv lex_eof)" # THE DOCUMENTED ERROR HOOK. `t_error' is called with the token whose first # character matched no rule; `lexer.skip(1)' is upstream's own recovery idiom. # Both characters are reported WITH THEIR POSITIONS, which is what a caller # reporting an error to a user needs and what a hook called with the wrong token # would get wrong. check_eq $P/lexerr-hits "\$@2,?@6" "$(pv lexerr_hits)" # ...AND LEXING CONTINUED PAST BOTH. This is the half that matters: a hook that # fired and then abandoned the input would satisfy the case above. All three # numbers on the far side of the two illegal characters are still delivered. check_eq $P/lexerr-survivors "NUMBER:1,NUMBER:2,NUMBER:3" "$(pv lexerr_survivors)" # LEX WRITES NO TABLE BY DEFAULT. See the header: this is NOT the symmetry a # reader expects after yacc, and it is asserted so that a caller who made the # working directory read-only knows which of the two halves will complain. check_eq $P/lextab-default "no" "$(pv lextab_default_written)" # ...and with `optimize' set it DOES, into `outputdir', on the same terms as # yacc -- written on the first build, left alone on the second. check_eq $P/lextab-opt "yes" "$(pv lextab_opt_written)" check_eq $P/lextab-opt-reuse "yes" "$(pv lextab_reuse_mtime_unchanged)" # ...and the optimized lexer still tokenises, which is what stops the two cases # above from being satisfied by a build that produced a file and no lexer. check_eq $P/lextab-opt-tokens "NUMBER:7,PLUS:+,NAME:x" "$(pv lextab_opt_tokens)" # ----------------------------------------------- where the parser writes --- # THE READ-ONLY ROOT, NAMED. See the header: this is the path `yacc()' would # have written to had the probe not passed `outputdir', and it is on the # virtiofs root. The case exists so that the redirection below is asserted to be # doing something rather than merely arranged. check_eq $P/outputdir-default "$BST_DATA" "$(pv outputdir_default)" check_eq $P/outputdir-used "$W" "$(pv outputdir_used)" # THE SECOND CONDITION ON REUSE, and the one the `reuse-*' group rests on: # `read_table' is an `__import__', so the working directory has to be on # sys.path or the table is written every run and read on none. check_eq $P/workdir-on-path "yes" "$(pv workdir_on_path)" # WHAT yacc GENERATED, as files on disk rather than as the probe's report of # them -- the suite looks for itself, in the directory the probe named. check $P/tab-file test -s "$W/plycalctab.py" check $P/debugfile test -s "$W/parser.out" check_eq $P/tab-written "yes" "$(pv tab_written)" check_eq $P/debugfile-written "yes" "$(pv debugfile_written)" # The generated module carries the grammar SIGNATURE, which is the whole # mechanism behind reuse: it is what a later build compares against. check_eq $P/tab-signature "yes" "$(pv tab_signature_line)" # ...and the table's own recorded format version, read off the generated file, # which must equal the `__tabversion__' asserted above. A mismatch is what makes # yacc discard a table, so these two agreeing is the precondition for reuse. check_out $P/tab-tabversion "_tabversion = '3.10'" cat "$W/plycalctab.py" # `parser.out' is the human-readable dump, and it is written because `debug' # defaults to ON. Its first line names the grammar it describes, which is what # says this file belongs to this parser rather than to a previous run. check_out $P/debugfile-grammar "Grammar" cat "$W/parser.out" check_eq $P/yacc-debug-default "yes" "$(pv yacc_debug_default)" # The module-level defaults, pinned. They are what a caller who passes no # `tabmodule' and no `debugfile' gets -- and, per the header, where an # unredirected write would land. check_eq $P/yacc-tab-module "parsetab" "$(pv yacc_tab_module)" check_eq $P/yacc-debug-file "parser.out" "$(pv yacc_debug_file)" # ------------------------------------------------------------- the grammar --- # THE PARSER, EVALUATED. Ten expressions and the values the grammar's own # semantic actions compute for them. Between them they cover every feature the # precedence declaration exists for, and each is asserted against a literal # rather than against a second computation of the same thing. # # `/' is Python 3's TRUE division, so a quotient is a float and prints with its # `.0'. That is the action's arithmetic, not the parser's, and the literals # below say so rather than hiding it behind an int(). check_eq $P/calc-add "5" "$(pv calc_add)" # PRECEDENCE. `2 + 3 * 4' is 14 and not 20 -- the single case that says the # precedence table was read at all. check_eq $P/calc-prec "14" "$(pv calc_prec)" check_eq $P/calc-prec-div "5.0" "$(pv calc_prec-div)" # LEFT ASSOCIATIVITY, for both levels of it. `10 - 3 - 2' is 5 under left # association and 9 under right; `100 / 5 / 2' is 10.0 against 40.0. Subtraction # and division are the two operators where the two answers differ, which is why # these and not `+' and `*'. check_eq $P/calc-assoc-left-minus "5" "$(pv calc_assoc-left-minus)" check_eq $P/calc-assoc-left-div "10.0" "$(pv calc_assoc-left-div)" # PARENTHESES, which override all of it: 20 rather than the 14 of `calc-prec'. check_eq $P/calc-paren "20" "$(pv calc_paren)" # UNARY MINUS, which is the reason the grammar has a `%prec UMINUS' annotation # at all: `MINUS expression' is ambiguous with the binary rule, and without the # annotation the production would inherit the precedence of `PLUS'/`MINUS'. # `- 2 * 5' is -10 either way, so `uminus-prec' is not the discriminating case; # `- 3 + 10' being 7 rather than -13 is. check_eq $P/calc-uminus "7" "$(pv calc_uminus)" check_eq $P/calc-uminus-prec "-10" "$(pv calc_uminus-prec)" check_eq $P/calc-uminus-paren "-5" "$(pv calc_uminus-paren)" # ...and it NESTS, which a right-associative rule must and a left-associative # one cannot: `- - 4' is 4. check_eq $P/calc-uminus-double "4" "$(pv calc_uminus-double)" # THE SYMBOL TABLE, which is the grammar's only state and the one thing that # makes a sequence of parses more than a sequence of calculations. The # assignment production writes it; the NAME production reads it. check_eq $P/assign-result "42" "$(pv assign_result)" # ...READ BACK BY A LATER, SEPARATE parse(). This is the case that says the # state survived the parse that set it. check_eq $P/assign-readback "43" "$(pv assign_readback)" check_eq $P/assign-chain "21.0" "$(pv assign_chain)" check_eq $P/names-table "x=42,y=21.0" "$(pv names_table)" # AN UNSET NAME resolves to the sentinel the grammar's own action defines rather # than raising, which is what keeps a typo in a caller's input a data problem # rather than a crash. check_eq $P/undefined-name "" "$(pv undefined_name)" # --------------------------------------------------------- the error paths --- # A SYNTAX ERROR CALLS p_error AND DOES NOT PROPAGATE. This is the contract the # ticket names, and it is the important half: a caller does not have to wrap # parse() in a try block. check_eq $P/synerr-raises "no" "$(pv synerr_raised)" check_eq $P/synerr-hook "at:+" "$(pv synerr_hook)" # ...AND parse() DOES NOT RETURN None. See the header: after `p_error' yacc # enters ERROR RECOVERY and discards tokens until the parse can continue, which # over `2 + + 3' leaves the trailing `3' to be reduced. So the result is 3, and # a caller testing `if result is None' to detect a syntax error would conclude # none had occurred. Upstream 3.11's behaviour, not this platform's, and pinned # here rather than left to be discovered. check_eq $P/synerr-result "3" "$(pv synerr_result)" # AT END OF INPUT it is the other branch: there is nothing left to recover with, # so parse() DOES return None, and the hook is called with `None' rather than a # token -- which is where a caller who wrote `p.value' unguarded crashes. check_eq $P/synerr-eof-result "None" "$(pv synerr_eof_result)" check_eq $P/synerr-eof-hook "at:EOF" "$(pv synerr_eof_hook)" # ------------------------------------------------------------- table reuse --- # THE TABLE IS REUSED, NOT REGENERATED. yacc writes a grammar signature into the # generated module and rebuilds only when it stops matching, so a second build # of the same grammar must leave the files ALONE. The mtime is the assertion # because it is the only thing that distinguishes a read from a rewrite of # identical bytes -- and it is BACKDATED before the second build, since two # writes inside one mtime tick would otherwise compare equal and pass. check_eq $P/reuse-mtime "yes" "$(pv reuse_mtime_unchanged)" check_eq $P/reuse-size "yes" "$(pv reuse_size_unchanged)" # `parser.out' is written on the same branch as the table module, so it is a # second, independent witness that the regeneration path was not entered. check_eq $P/reuse-debugfile "yes" "$(pv reuse_debugfile_unchanged)" # ...and the parser built from the reused table still parses. Without this the # three cases above would be satisfied by a build that read the file and # produced nothing usable. check_eq $P/reuse-parses "14" "$(pv reuse_parses)" # THE CONTROL FOR ALL FOUR. See the header: an assertion that nothing happened # is passed trivially by a build that never looked at the file. Corrupt the # recorded signature, build the IDENTICAL grammar again, and the table must be # rewritten -- which is what says the signature is being read and compared # rather than the file merely being present. check_eq $P/stale-regenerated "yes" "$(pv stale_regenerated)" # ...and what came back is the RIGHT table, not merely a newer file. check_eq $P/stale-signature-restored "yes" "$(pv stale_signature_restored)" rm -rf "$W"