. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # third start left behind. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for a hundred and thirty-three # # measurements. # # # # The work inside each start is a character-by-character # # walk over inputs under forty characters, plus one module # # reload at the tail. Microseconds beside an interpreter # # start on this arm, which is tens of seconds before the # # first line of the program runs. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-imagesize sets for its three, and roughly six # # times the ~60s a python3 start has measured at here. # # Nothing in this suite is quadratic in anything, so the # # start is the only thing the number has to cover. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # 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-expandvars 0.12.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-expandvars_0.12.0.bb, `inherit pypi # python_hatchling python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs # glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # `expandvars(s)' expands shell-style variable references in a string against # os.environ, and `expand(s, environ=..., nounset=..., var_symbol=...)' does the # same against a caller-supplied Mapping. It is a hand-written recursive # descent over the characters -- no regex, no shell -- and it covers `$VAR', # `${VAR}', `${VAR:-default}', `${VAR:=default}', `${VAR:?message}', # `${VAR:+alternate}', the offset/length form `${VAR:off:len}', indirection # `${!PTR}', `$$', backslash escaping, and a `nounset' mode modelled on # `set -u'. # # Ten exported names, one 469-line module, no configure step, no compiled # extension and no C anywhere in the package. # # ============================================================================ # THE HEADLINE FINDING: `${#VAR}' IS NOT IMPLEMENTED AND DOES NOT SAY SO. # ============================================================================ # # The length operator is absent, and its absence is silent. `#' is not a valid # variable-name character, so `${#SET}' parses as a variable named "" carrying # the modifier "#SET"; that modifier begins with none of `-=+?', so it falls to # the offset branch, where "#SET" is not an integer and is taken as an offset of # ZERO -- and `getenv("")' finds nothing and returns "". The whole expression # evaluates to the empty string, for every variable, set or not, with no # diagnostic anywhere. # # `hash-length-set', `hash-length-unset' and `hash-length-empty' assert that # empty string directly, and `len-via-slice' sits beside them with the same # length taken the way that works. This matters more than an ordinary gap # because `${#VAR}' is the one operator here that LOOKS like it must be # supported -- eight of the ten `__all__' names are exceptions, so the library # advertises itself as strict, and a caller who writes `int(expand("${#VAR}"))' # gets a ValueError from `int("")' one frame away from the real cause. # # `hash-length-nounset' is the one setting that catches it at all, and it # reports a DIFFERENT bug: `${#SET}' under `nounset=True' raises # `UnboundVariable' whose message is ": unbound variable" -- no name in front of # the colon, because the variable it could not find is the empty one. # `hash-length-nounset-msg' asserts that message, which is the proof of the # misparse rather than an inference from it. # # ============================================================================ # THE COLON IS NOT READ, SO `:-' AND `-' ARE THE SAME OPERATOR. # ============================================================================ # # In POSIX and in bash the colon is significant: `${VAR:-d}' substitutes for # unset OR empty, `${VAR-d}' only for unset. Here `expand_modifier_var' steps # PAST a `:' before dispatching on the operator character, so the two forms # reach identical code -- and `getenv' tests the value for TRUTH rather than # presence, so both treat empty-but-set as unset. # # That single cause produces four separate divergences, and each is asserted on # its own rather than left to be inferred from the others: # # default-nocolon-empty `${EMPTY-d}' -> "d" (bash: "") # assign-nocolon-empty `${EMPTY=v}' -> "v", AND WRITES (bash: no write) # alt-nocolon-empty `${EMPTY+alt}' -> "" (bash: "alt") # nounset-empty `$EMPTY' under nounset RAISES (set -u: "") # # `default-colon-agrees' asserts the equality itself, so a release that started # reading the colon turns that case red rather than leaving four scattered ones # to be re-diagnosed. # # ============================================================================ # `${VAR:?msg}' RAISES `ParameterNullOrNotSet', NOT `UnboundVariable'. # ============================================================================ # # The two are different classes reached by different paths. `${VAR:?}' goes # through `expand_strict' and always raises `ParameterNullOrNotSet'; # `UnboundVariable' comes out of `getenv' and is reachable ONLY under # `nounset=True'. A caller who wraps a `${VAR:?msg}' template in # `except UnboundVariable' catches nothing -- which is worth pinning because the # two names describe the same situation in English. # # `strict-not-unbound' asserts that neither is a subclass of the other, and both # messages are asserted verbatim, because a caller who cannot rely on the class # will match on the text. # # ============================================================================ # THE OTHER SURPRISES, EACH ASSERTED WHERE IT SITS. # ============================================================================ # # `$$' IS THE PID. `expand_var' substitutes `os.getpid()' for a second # var_symbol, so a template containing `$$' is process-dependent. That is the # bash behaviour and is a surprise in a library whose output is otherwise a # pure function of its input. `pid-expands' asserts it against the probe's own # pid rather than against a constant. # # THE TWO LENIENT PARSES DISAGREE ABOUT THEIR FALLBACK. A non-numeric OFFSET # is silently zero, so `${SET:x}' returns the whole string; a non-numeric but # alphanumeric LENGTH is silently None, and `expand_length' then slices # `[offset:0]', so `${SET:0:x}' returns NOTHING. Neither is refused, and they # fall back opposite ways. # # `:=' WRITES INTO THE CALLER'S OWN MAPPING. `expand_default' calls # `environ.update' on the object it was handed, so a caller who passes a plain # dict to `expand' gets that dict mutated. Every `assign-*' case takes its own # copy and asserts the mapping afterwards as well as the return value. # # A SYNTAX ERROR NAMES THE WHOLE INPUT. `expand' catches its own three syntax # exceptions at the top level and re-raises them over `vars_', so the # diagnostic for a malformed expression buried in a long template is the # entire template. `bad-msg-whole-input' asserts that. # # `MissingExcapedChar' IS SPELLED THAT WAY. It is a public name in `__all__', # so fixing the typo would break every caller that catches it. Pinned in # `all-names' rather than tidied in a comment. # # EXPANDVARS_RECOVER_NULL DISARMS BOTH STRICT MODES. It is read at import time # and turns `${VAR:?}' and `nounset=True' into a constant. The `recover-null-*' # cases are LAST in the probe because reaching them costs a module reload; # upstream's own docstring warns against exporting the variable, and these # cases are what that warning is about. # # ============================================================================ # NO PATCH AND NO SITE ANSWER. # ============================================================================ # # The recipe is taken from meta-python exactly as written -- the port is one # IMAGE_INSTALL line, this suite and its probe, with no bbappend at all. There # is no configure step, no compile step and no C in the package, so there is # nothing on this arm for an override to reach. MEASURED: the recipe-scope build # ran 1858 tasks, all succeeded, zero ERROR: lines -- and the installed # `expandvars.py' is BYTE-IDENTICAL to the sdist the SRC_URI sha256 pins (`cmp' # against the unpacked tarball), which is the measurement behind "no patch" # rather than a reading of the recipe. # # PURE PYTHON, WHICH IS WHY IT PORTS. This image's python3 is ONE STATIC BINARY # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a module # carrying a compiled extension could not be imported here at all. The whole # payload is one 12922-byte file, and the .deb carries no .so and nothing that # is not a .py, a .pyc or dist-info metadata. # # THE RDEPENDS IS `python3-core' ALONE, AND IT IS CORRECT. The module's only # imports are `os' and `from io import TextIOWrapper', both of which # python3-core carries -- and so is `importlib.metadata', which # `dist-version' below reaches through. CHECKED against the built .deb's own # control file and against poky's python3 split rather than assumed: unlike the # neighbouring ports there is no under-declared split here and no bbappend is # needed. # # ============================================================================ P=python3-expandvars # The canary is first so that a suite which dies before its own first assertion # is distinguishable from one that never started. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages MOD=$SITE/expandvars.py DI=$SITE/expandvars-0.12.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-expandvars.list PROBE=$BST_DATA/python3-expandvars-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-expandvars.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A FLAT MODULE, NOT A PACKAGE: site-packages holds `expandvars.py' and no # `expandvars/' directory. A release that split it into a package would still # import and still pass every behavioural case below, so the shape is asserted # on its own. check $P/site-packages-dir test -d "$SITE" check $P/module-file test -s "$MOD" check $P/no-package-dir test ! -d "$SITE/expandvars" # Upstream's `tests/' directory is not packaged -- hatchling builds the wheel # from `expandvars.py' alone. This is a size assertion as much as a layout one. check $P/no-test-dir test ! -d "$SITE/tests" check $P/no-py-typed test ! -e "$SITE/expandvars.pyi" check $P/pycache-module test -s "$SITE/__pycache__/expandvars.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". This image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying a compiled extension could not be imported here at all. # expandvars ships no C -- no .c, no .pyx, no ext_modules -- so this is a # REGRESSION TRIPWIRE on a version bump rather than a statement about today. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python in the payload, which is the same claim from the # other side: a data file or a script appearing here would be a packaging change # nobody asked for. _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 PACKAGE INSTALLS NO EXECUTABLE. `pyproject.toml' declares no # `[project.scripts]', so there is nothing in ${bindir} and nothing to reach # through a shebang -- the library is importable only. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.12.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: expandvars" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `python_hatchling' BUILT THIS, WHICH IS VISIBLE IN THE WHEEL METADATA and is # the half of the recipe's `inherit' line that leaves a trace on the rootfs. # Three things distinguish it from the `setuptools3' ports beside it: the # generator names hatchling, there is NO `top_level.txt', and the licence lands # under `licenses/' rather than flat in the dist-info. The version in # `dist-info-generator' is a property of this image's hatchling pin rather than # of this package -- eight other hatchling-built packages are already on the # image and a bump moves the string for all nine at once. check_out $P/dist-info-generator "Generator: hatchling 1.21.1" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check $P/dist-info-no-top-level test ! -e "$DI/top_level.txt" check $P/dist-info-license test -s "$DI/licenses/LICENSE" check $P/dist-info-no-flat-license test ! -e "$DI/LICENSE" # The RECORD names exactly five entries and one payload file. A repackaging that # started shipping a second module would show up here first. check_out $P/record-module "expandvars.py,sha256=" cat "$DI/RECORD" check_eq $P/record-lines "5" "$(grep -c . "$DI/RECORD" 2>/dev/null)" sed -n '/^Package: python3-expandvars$/,/^$/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: 0.12.0-r0" cat "$W/st" # THE ONE RDEPENDS IS A SUB-PACKAGE OF THE PORTED python3 (BRINTOS-1476) and is # already on this image. `python3-core' comes from the python3 packaging and # carries `os', `io' and `importlib.metadata', which are the module's entire # import surface. The line is asserted exactly, so a leaner image that split # any of those out would say so here once rather than present as a broken # library. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import expandvars' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. check_eq $P/import-quiet "" \ "$(python3 -c 'import expandvars' 2>&1)" # ----------------------------------------------------------------- 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 bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. # # EVERY STRING RESULT BELOW IS WRAPPED IN ANGLE BRACKETS, and `<>' is the empty # string. That is the probe's convention and it is load-bearing here: this # library returns "" for a great many inputs -- including the `${#VAR}' finding # this suite exists for -- and without the wrapper `grep | cut' could not tell # an empty ANSWER from a key the probe never reached. A refusal is `!'. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "133" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # module named here. check_eq $P/module-answered "<$MOD>" "$(pv module_file)" check_eq $P/is-flat-module "no" "$(pv is_package)" # `__version__' CARRIES A LEADING `v' AND THE DIST VERSION DOES NOT -- "v0.12.0" # against "0.12.0". Both are asserted, because a caller that compares the # attribute against a requirement string gets a silent mismatch. check_eq $P/version-attr "" "$(pv version_attr)" check_eq $P/dist-version "<0.12.0>" "$(pv dist_version)" check_eq $P/author "" "$(pv author)" # THE EXPORTED SURFACE IS TEN NAMES, EIGHT OF THEM EXCEPTIONS -- which is what # makes the silent `${#VAR}' hole above so out of character. The list is the # contract, so it is asserted in order, and `MissingExcapedChar' is spelled # exactly as upstream spells it. check_eq $P/all-names \ "" \ "$(pv all_names)" check_eq $P/all-count "10" "$(pv all_count)" check_eq $P/has-expand "yes" "$(pv has_expand)" check_eq $P/has-expandvars "yes" "$(pv has_expandvars)" check_eq $P/has-base-exc "yes" "$(pv has_ExpandvarsException)" check_eq $P/has-typo-name "yes" "$(pv has_MissingExcapedChar)" # EVERY EXCEPTION IS BOTH `ExpandvarsException' AND A BUILT-IN, which is what # lets a caller who never imported this library still catch what it raises -- # and is the only reason the `ParameterNullOrNotSet'/`UnboundVariable' confusion # below is survivable, since both are `KeyError'. check_eq $P/exc-base-brace "yes" "$(pv base_MissingClosingBrace)" check_eq $P/exc-builtin-brace "SyntaxError" "$(pv builtin_MissingClosingBrace)" check_eq $P/exc-builtin-escape "SyntaxError" "$(pv builtin_MissingExcapedChar)" check_eq $P/exc-builtin-operand "SyntaxError" "$(pv builtin_OperandExpected)" check_eq $P/exc-builtin-badsub "SyntaxError" "$(pv builtin_BadSubstitution)" check_eq $P/exc-builtin-negsub "IndexError" "$(pv builtin_NegativeSubStringExpression)" check_eq $P/exc-builtin-param "KeyError" "$(pv builtin_ParameterNullOrNotSet)" check_eq $P/exc-builtin-unbound "KeyError" "$(pv builtin_UnboundVariable)" # ------------------------------------------------------------ plain forms ---- check_eq $P/plain-set "" "$(pv plain_set)" check_eq $P/plain-unset "<>" "$(pv plain_unset)" check_eq $P/plain-empty "<>" "$(pv plain_empty)" check_eq $P/braced-set "" "$(pv braced_set)" check_eq $P/braced-suffix "" "$(pv braced_suffix)" # A bare `$VAR' ends at the first character that is not alphanumeric or `_'... check_eq $P/bare-suffix "" "$(pv bare_suffix)" # ...and `_' IS one, so `$SET_X' is a different variable from `$SET' followed by # "_X". The pair is the whole of the name-scanning rule. check_eq $P/bare-underscore "" "$(pv bare_underscore)" check_eq $P/two-vars "" "$(pv two_vars)" check_eq $P/literal-text "" "$(pv literal_text)" check_eq $P/empty-input "<>" "$(pv empty_input)" # A TRAILING `$' WITH NOTHING AFTER IT IS RETURNED AS ITSELF rather than # refused, which is the one malformed input in the suite that does not raise. check_eq $P/lone-dollar "<\$>" "$(pv lone_dollar)" # ------------------------------------------------------- `-' and `:-' -------- # See the header: the colon is stepped past, so these six are three pairs. check_eq $P/default-colon-unset "" "$(pv default_colon_unset)" check_eq $P/default-colon-set "" "$(pv default_colon_set)" check_eq $P/default-colon-empty "" "$(pv default_colon_empty)" check_eq $P/default-nocolon-unset "" "$(pv default_nocolon_unset)" check_eq $P/default-nocolon-set "" "$(pv default_nocolon_set)" # THE DIVERGENCE. bash gives "" here, because `-' without the colon substitutes # only for an UNSET variable and EMPTY is set. check_eq $P/default-nocolon-empty "" "$(pv default_nocolon_empty)" # ...asserted as an equality too, so a release that started reading the colon # turns ONE case red rather than four scattered ones. check_eq $P/default-colon-agrees "yes" "$(pv default_colon_agrees)" # An empty default is a default, not a missing one -- `${VAR:-}' returns "" and # does not take the `BadSubstitution' path that `${VAR:}' takes below. check_eq $P/default-empty-modifier "<>" "$(pv default_empty_modifier)" # The default is itself expanded, in both spellings. check_eq $P/default-nested "" "$(pv default_nested)" check_eq $P/default-nested-var "" "$(pv default_nested_var)" # --------------------------------------------------------- `=' and `:=' ------ # THE ONE OPERATOR WITH A SIDE EFFECT, and the mapping it writes into is the # CALLER'S -- see the header. Each case asserts the return value and the mapping # afterwards, because the two can disagree. check_eq $P/assign-unset "" "$(pv assign_unset)" check_eq $P/assign-unset-stored "" "$(pv assign_unset_stored)" check_eq $P/assign-set "" "$(pv assign_set)" check_eq $P/assign-set-stored "" "$(pv assign_set_stored)" check_eq $P/assign-empty "" "$(pv assign_empty)" check_eq $P/assign-empty-stored "" "$(pv assign_empty_stored)" # THE DIVERGENCE, and the costly one of the four: bash's `${VAR=v}' leaves a # set-but-empty variable alone, and this WRITES to it. check_eq $P/assign-nocolon-empty "" "$(pv assign_nocolon_empty)" check_eq $P/assign-nocolon-empty-stored "" "$(pv assign_nocolon_empty_stored)" # ------------------------------------------------------------ `+' and `:+' --- check_eq $P/alt-colon-set "" "$(pv alt_colon_set)" check_eq $P/alt-colon-unset "<>" "$(pv alt_colon_unset)" check_eq $P/alt-colon-empty "<>" "$(pv alt_colon_empty)" # THE DIVERGENCE, this one pointing the other way: bash gives "alt". check_eq $P/alt-nocolon-empty "<>" "$(pv alt_nocolon_empty)" # ------------------------------------------------------------ `?' and `:?' --- # `ParameterNullOrNotSet', NOT `UnboundVariable' -- see the header. check_eq $P/strict-unset "!" "$(pv strict_unset)" check_eq $P/strict-empty "!" "$(pv strict_empty)" check_eq $P/strict-set "" "$(pv strict_set)" check_eq $P/strict-nocolon "!" "$(pv strict_nocolon)" # The messages, verbatim: a caller who cannot rely on the class will match text. check_eq $P/strict-msg "" "$(pv strict_msg)" check_eq $P/strict-msg-default "" "$(pv strict_msg_default)" check_eq $P/strict-not-unbound "yes" "$(pv strict_not_unbound)" # ----------------------------------------------------------------- nounset --- check_eq $P/nounset-set "" "$(pv nounset_set)" check_eq $P/nounset-unset "!" "$(pv nounset_unset)" check_eq $P/nounset-braced "!" "$(pv nounset_braced)" # THE DIVERGENCE. `set -u' does not fire on a variable that is set and empty; # this does, because `getenv' tests for truth. check_eq $P/nounset-empty "!" "$(pv nounset_empty)" check_eq $P/nounset-msg "" "$(pv nounset_msg)" # A default disarms `nounset' -- `getenv' returns the default before it consults # the flag at all... check_eq $P/nounset-default "" "$(pv nounset_default)" # ...and `+' is unaffected for a different reason: `expand_substitute' never # calls `getenv'. Two operators, one answer, two causes. check_eq $P/nounset-alt "<>" "$(pv nounset_alt)" # -------------------------------------------------------- offset and length -- # A NEGATIVE OFFSET NEEDS A LEADING SPACE, exactly as in bash and for the same # reason: `${SET:-3}' is the default operator, not an offset of -3. check_eq $P/offset-positive "" "$(pv offset_positive)" check_eq $P/offset-zero "" "$(pv offset_zero)" check_eq $P/offset-past-end "<>" "$(pv offset_past_end)" check_eq $P/offset-negative "" "$(pv offset_negative)" check_eq $P/offset-len "" "$(pv offset_len)" check_eq $P/offset-len-overlong "" "$(pv offset_len_overlong)" check_eq $P/offset-neg-len "" "$(pv offset_neg_len)" check_eq $P/len-negative "!" "$(pv len_negative)" check_eq $P/len-negative-msg "" "$(pv len_negative_msg)" # THE TWO LENIENT PARSES FALL BACK OPPOSITE WAYS -- see the header. A # non-numeric offset is zero, so the whole string comes back... check_eq $P/offset-nonnumeric "" "$(pv offset_nonnumeric)" # ...and a non-numeric alphanumeric length is None, which `expand_length' turns # into a `[offset:0]' slice, so NOTHING comes back. Asserted at both offsets so # that it is visibly the width and not the offset that collapses. check_eq $P/len-alnum "<>" "$(pv len_alnum)" check_eq $P/len-offset-alnum "<>" "$(pv len_offset_alnum)" # A length that is neither an integer nor alphanumeric IS refused, which is # where the lenience stops. check_eq $P/len-operand "!" "$(pv len_operand)" check_eq $P/len-operand-msg "" \ "$(pv len_operand_msg)" # ------------------------------------------------------- the `#' operator ---- # THE HEADLINE FINDING. See the header for the parse that produces it. check_eq $P/hash-length-set "<>" "$(pv hash_length_set)" check_eq $P/hash-length-unset "<>" "$(pv hash_length_unset)" check_eq $P/hash-length-empty "<>" "$(pv hash_length_empty)" # The one setting that catches it reports a DIFFERENT bug, and the message is # the proof: no name in front of the colon, because the variable it could not # find is the empty one. check_eq $P/hash-length-nounset "!" "$(pv hash_length_nounset)" check_eq $P/hash-length-nounset-msg "<: unbound variable>" "$(pv hash_length_nounset_msg)" # The same length taken the way that works, so the gap is explicit rather than # implied. check_eq $P/len-via-slice "" "$(pv len_via_slice)" # ---------------------------------------------------------------- escaping --- check_eq $P/escape-dollar "<\$SET>" "$(pv escape_dollar)" # `\\$VAR' IS A LITERAL BACKSLASH FOLLOWED BY THE EXPANSION -- the escape # consumes itself and hands the `$' back to the expander. check_eq $P/escape-backslash "<\\hello world>" "$(pv escape_backslash)" # A BACKSLASH BEFORE AN ORDINARY CHARACTER IS DROPPED, not preserved: `\n' comes # back as the single character `n'. Nothing in the README says so, and it means # a Windows path in a template loses its separators. check_eq $P/escape-other "" "$(pv escape_other)" # ...but a TRAILING backslash raises rather than being dropped, so the lenience # above is not uniform. check_eq $P/escape-trailing "!" "$(pv escape_trailing)" check_eq $P/escape-trailing-msg "" \ "$(pv escape_trailing_msg)" check_eq $P/escape-only "!" "$(pv escape_only)" check_eq $P/escape-brace "<\${SET}>" "$(pv escape_brace)" # ----------------------------------------------------------- indirection ----- check_eq $P/indirect "" "$(pv indirect)" check_eq $P/indirect-unset "<>" "$(pv indirect_unset)" check_eq $P/indirect-missing "<>" "$(pv indirect_missing)" # A dangling pointer takes the default rather than leaking the pointer's own # value through as a name. check_eq $P/indirect-default "" "$(pv indirect_default)" # ----------------------------------------------------------- `$$' is the PID -- # NOT a literal dollar -- see the header. Asserted against the probe's own pid. check_eq $P/pid-expands "yes" "$(pv pid_expands)" check_eq $P/pid-not-literal "yes" "$(pv pid_not_literal)" # ------------------------------------------------------------ var_symbol ----- # The symbol is a parameter, which is what lets the library rewrite a file whose # own syntax already uses `$'. check_eq $P/symbol-percent "" "$(pv symbol_percent)" check_eq $P/symbol-dollar-literal "<\$HOME hello world>" "$(pv symbol_dollar_literal)" check_eq $P/symbol-braced "" "$(pv symbol_braced)" # ------------------------------------------------------------- malformed ----- check_eq $P/bad-empty-braces "!" "$(pv bad_empty_braces)" check_eq $P/bad-empty-modifier "!" "$(pv bad_empty_modifier)" check_eq $P/bad-msg "<\${SET:}: bad substitution>" "$(pv bad_msg)" check_eq $P/missing-brace "!" "$(pv missing_brace)" check_eq $P/missing-brace-msg "<\${SET: missing '}'>" "$(pv missing_brace_msg)" # Brace depth is counted, so an inner `${...}' does not close the outer one -- # this input is short by ONE brace, not by none. check_eq $P/missing-brace-nested "!" "$(pv missing_brace_nested)" # THE DIAGNOSTIC NAMES THE WHOLE INPUT, not the failing fragment -- see header. check_eq $P/bad-msg-whole-input "" \ "$(pv bad_msg_whole_input)" # ...and the depth counting working, from the other side. check_eq $P/nested-depth "" "$(pv nested_depth)" # ------------------------------------------------- the os.environ entry point -- # `expandvars()' is `expand()' bound to os.environ, and these are the only cases # in the suite that touch the real environment. check_eq $P/ev-set "" "$(pv ev_set)" check_eq $P/ev-unset "<>" "$(pv ev_unset)" check_eq $P/ev-nounset "!" "$(pv ev_nounset)" check_eq $P/ev-default "" "$(pv ev_default)" # `expand's DEFAULT `environ' ARGUMENT IS THE LIVE os.environ OBJECT, bound once # at def time -- so it tracks later mutations rather than snapshotting them at # import. A mutable default that happens to be correct. check_eq $P/ev-default-arg-is-live "" "$(pv ev_default_arg_is_live)" # ------------------------------------------------------------- file input ---- # `expand' ACCEPTS AN OPEN FILE and `.strip()'s what it reads, so the trailing # newline every text file has does not reach the output... check_eq $P/file-input "" "$(pv file_input)" # ...but the type test is `isinstance(vars_, TextIOWrapper)', so an io.StringIO # is NOT accepted: it is file-like and is not that class, and the refusal comes # out as a bare TypeError from `len()' rather than as one of this library's own. check_eq $P/stringio-input "!" "$(pv stringio_input)" # --------------------------------------------------------- RECOVER_NULL ------ # LAST, because reaching these costs a module reload -- see the header. What # EXPANDVARS_RECOVER_NULL does is turn BOTH strict paths into a constant, which # is why upstream's own docstring warns against exporting it. check_eq $P/recover-null-value "" "$(pv recover_null_value)" check_eq $P/recover-null-strict "" "$(pv recover_null_strict)" check_eq $P/recover-null-nounset "" "$(pv recover_null_nounset)" # It does NOT reach the ordinary unset path, which returned "" already. check_eq $P/recover-null-plain "<>" "$(pv recover_null_plain)" rm -rf "$W"