. "$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 seventy-eight measurements. # # # # The work inside the probe is building five small config # # files in tmpfs and parsing a few dozen short strings. # # 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-pathtools3 and 99-python3-imagesize set for # # their 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. # # # # 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-configargparse 1.7 (meta-openembedded/meta-python # recipes-devtools/python/python3-configargparse_1.7.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # ONE FLAT MODULE, 1489 lines, no package directory and no compiled extension. # `configargparse.ArgumentParser' subclasses `argparse.ArgumentParser' and the # module re-exports the whole of argparse, so a caller who swaps # `import argparse' for `import configargparse' keeps every name they had -- # that is what "drop-in" means here and `reexport-*' asserts it name by name. # # What it ADDS is that an option's value may also come from a CONFIG FILE or an # ENVIRONMENT VARIABLE, with a documented precedence: # # command line > environment variable > config file > default # # plus `format_values()', which reports where each value actually came from, and # `--write-out-config-file', which writes the effective configuration back out. # # ============================================================================ # THE PORT IS ONE IMAGE_INSTALL LINE, THIS SUITE, ITS PROBE -- AND ONE bbappend. # ============================================================================ # # There is no patch and no site answer: there is no configure step, no compile # step and no C anywhere in the package, so there is nothing on this arm for an # override to reach. MEASURED rather than read off the recipe -- the installed # `configargparse.py' is BYTE-IDENTICAL to the sdist the SRC_URI sha256 pins. # # THE bbappend DOES ONE THING: `PACKAGECONFIG = ""'. meta-python's recipe carries # `PACKAGECONFIG ?= "yaml"' with `PACKAGECONFIG[yaml] = ",,,python3-pyyaml"', so # the stock recipe would put python3-pyyaml on the RDEPENDS of every image that # installs this package. Upstream declares `install_requires=[]' and puts PyYAML # in `extras_require["yaml"]' -- an EXTRA -- and poky's python3-pyyaml builds a # CYTHON EXTENSION against libyaml, which this image's python3 could not import # at all: it is one static binary with an empty lib-dynload and no dynamic loader # (BRINTOS-1476). So the extra is declined, and the one behaviour that removes is # asserted rather than left implicit -- see `yaml-parse' below. # # THE DECLARED RDEPENDS IS RIGHT AND IS LEFT ALONE. CHECKED against poky's # python3-manifest rather than assumed: the module's entire module-scope import # surface is argparse, ast, csv, functools, json, glob, os, re, sys, types, # collections, textwrap and io. glob.py is python3-shell and the json package is # python3-json -- both named by the stock recipe -- and every other one, including # `configparser', which the default config-file parser reaches lazily, is in # python3-core. The built .deb's line is exactly # "Depends: python3-core, python3-json, python3-shell" and `pkg-depends-exact' # pins it. # # ============================================================================ # THE HEADLINE FINDING: THE ENVIRONMENT DOES NOT OUTRANK THE CONFIG FILE -- IT # HIDES IT. # ============================================================================ # # The documented precedence is true of the RESULT, and `ladder-env-cfg-default' # measures it: with the same option in the environment and in the file, the # environment wins. But the MECHANISM is not a comparison. An env var is # converted to `--colour from-env' and PREPENDED TO argv before any config file # is opened; the config-file loop then asks `already_on_command_line' and, seeing # the option there, DISCARDS the file's copy entirely. # # So the config file is not a losing source, it is NOT A SOURCE AT ALL, and # `format_values()' -- the very call this ticket asks to report where each value # came from -- does not mention the file. `fv-env-shadows-config' asserts the # whole report, and `stsd-keys' asserts the same absence through # `get_source_to_settings_dict', whose keys are `command_line' and # `environment_variables' with no `config_file' among them. # # A caller reading that report to answer "which of my config files was used" # gets silence rather than a losing entry, and there is nothing in the output to # say the file was read at all. That is the finding most likely to reach someone, # because the report is the debugging tool -- it is what you turn to precisely # when a config file appears to have been ignored. # # ============================================================================ # A CONFIG FILE NAMING AN UNKNOWN OPTION IS NOT REFUSED BY THIS LIBRARY. # ============================================================================ # # The ticket asks what a config file naming an option the parser does not define # does. The answer is that configargparse does not decide: it converts the key to # `--nosuchoption=7' and puts it on argv, and ARGPARSE decides. So the same file # is a fatal `error: unrecognized arguments' under `parse_args' # (`unknown-key-parse-args-*') and a LEFTOVER under `parse_known_args' # (`unknown-key-leftover') -- one file, two outcomes, chosen by the entry point. # # And the file's other keys are applied either way: `unknown-key-colour-still- # parsed' is `from-config-eq', so the file is not discarded wholesale. The error # message names the SYNTHESISED `--nosuchoption=7' rather than the config file or # the line, so a caller reading it has nothing pointing back at the file. # # ============================================================================ # THE ROUND TRIP IS EXACT FOR EVERY OPTION THE FILE IS ABOUT, AND CANNOT BE # EXACT FOR THE WHOLE NAMESPACE. # ============================================================================ # # `--write-out-config-file' writes only what was NOT left at its default, and # writes neither the config-file option nor the write-out option itself -- a file # naming either would re-trigger it on the way back in. `woc-contents' pins the # two lines it does write and `woc-has-*' the three it does not. # # Fed back in, the namespace agrees on every real option (`woc-roundtrip-real- # equal'), and the WHOLE namespaces differ (`woc-roundtrip-ns-equal' is `no'): # `config' is None on the direct run and holds the file's path on the round trip. # That is a fact about how the values were supplied rather than a value the file # carries, and both are asserted so the inequality is a recorded property rather # than something a later reader has to rediscover. # # ============================================================================ # THE SMALLER SURPRISES, EACH ASSERTED WHERE IT SITS. # ============================================================================ # # version-attr the module has NO `__version__'. The version lives # only in the dist-info, so a caller who wants it must # go through importlib.metadata. # dist-version-lower ...and BOTH spellings of the distribution answer # there -- `ConfigArgParse' and `configargparse' -- # because importlib.metadata normalises the name. # cfp-empty-value `a =' is the EMPTY STRING and `a' alone is the # string "true". The two differ only in the # separator, and that is the whole of the bare-flag # syntax. # cfp-list-legacy `[apple, orange]' is not JSON; `json.loads' refuses # and a hand-rolled split takes over, so the legacy # spelling and the JSON one give the same list. # woc-stdout `write_config_file' PRINTS to stdout, not stderr, # and exits 0. A caller piping stdout gets the notice # mixed into their data. # missing-config-stderr a config file that is not there is an argparse # `error:' with exit 2, not an exception. # bad-line-stderr ...and so is a line the parser's regex refuses, # which is the one message that names the file AND # the line -- numbered from ZERO. # required-from-config a `required=True' option is satisfied from the # config file, which says the requirement is checked # AFTER the merge rather than against argv. # # ============================================================================ # ON TARGET: 78 probe measurements over three python3 starts. # ============================================================================ # # EVERY STRING RESULT IS WRAPPED IN ANGLE BRACKETS and the empty string is `<>'. # Load-bearing here: several answers are legitimately empty -- the leftovers of a # `parse_known_args' that recognised everything, the stderr of a call that did # not fail -- and `grep | cut' cannot otherwise tell an empty ANSWER from a key # the probe never reached. A refusal is `!'. # # MULTI-LINE VALUES ARE JOINED WITH `|', which is why the `fv-*' literals below # carry pipes: the runner parses its input line by line, so a value with an # embedded newline would land as one counted case plus N uncounted stray lines. # # THE PROBE'S OWN TEMPORARY DIRECTORY IS REPLACED BY THE LITERAL `WORKDIR' in # every value, because `mkdtemp' picks a fresh suffix every boot and the paths # appear inside `format_values()' output and inside two error messages. # # ============================================================================ P=python3-configargparse # 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/configargparse.py DI=$SITE/ConfigArgParse-1.7.dist-info PKGLIST=/var/lib/dpkg/info/python3-configargparse.list PROBE=$BST_DATA/python3-configargparse-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-configargparse.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A FLAT MODULE, NOT A PACKAGE. setup.py declares `py_modules= # ["configargparse"]', so site-packages holds one .py and no 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/flat-module test -s "$MOD" check $P/no-package-dir test ! -d "$SITE/configargparse" # THE SDIST SHIPS A `tests/' PACKAGE AND THE WHEEL DOES NOT, because `py_modules' # names one module and nothing else. Upstream's own suite is therefore not on the # rootfs, which is worth asserting: several packages beside this one on the image # DO ship their tests, and the difference is a packaging decision rather than a # property of the source. check $P/no-tests-dir test ! -d "$SITE/tests" # The .pyc ships in the .deb, which is what lets the module import on a READ-ONLY # root: nothing has to be compiled at first import. check $P/pycache test -s "$SITE/__pycache__/configargparse.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. # ConfigArgParse ships no C -- no .c, no .pyx, no ext_modules in setup.py -- 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. setup.py declares no `entry_points' and no # `scripts', so there is nothing in ${bindir}. The library is importable only -- # which is worth asserting for a package whose whole subject is command lines. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EXACTLY ONE .py FILE. A repackaging that started shipping upstream's `tests/' # would show up here first, before any behavioural case noticed. check_eq $P/pkg-py-count "1" \ "$(grep -c '/site-packages/[a-z_]*\.py$' "$W/pkgf" 2>/dev/null)" # THE DIST-INFO IS MIXED CASE. `PYPI_PACKAGE = "ConfigArgParse"', so the # directory is `ConfigArgParse-1.7.dist-info' while the module is all lower case. # Asserted by name, because a recipe that normalised PYPI_PACKAGE would move it # and every `$DI' case below would go red at once with no explanation. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.7" cat "$DI/METADATA" check_out $P/dist-info-name "Name: ConfigArgParse" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `setuptools3' BUILT THIS, and it leaves three traces the `python_hatchling' # ports beside it on this image do not: the generator names `bdist_wheel', there # IS a `top_level.txt', and the licence lands FLAT in the dist-info rather than # under `licenses/'. All three are asserted, so a recipe moved onto another # backend says so here rather than silently. check_out $P/dist-info-generator "Generator: bdist_wheel (0.42.0)" 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" # THE WHEEL IS NOT UNIVERSAL -- there is no `py2' tag, unlike several of the # 2018-era ports beside it. setup.py declares `python_requires=">=3.5"'. check_eq $P/dist-info-no-py2-tag "" \ "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check $P/dist-info-top-level test -s "$DI/top_level.txt" check_eq $P/dist-info-top-level-name "configargparse" "$(cat "$DI/top_level.txt" 2>/dev/null)" check $P/dist-info-flat-license test -s "$DI/LICENSE" check $P/dist-info-no-licenses-dir test ! -d "$DI/licenses" # THE METADATA STILL DECLARES THE `yaml' EXTRA, and that is exactly right: the # bbappend declines the extra, it does not remove it from upstream's metadata. # The line is what a caller reads to discover that YAML support exists and is # opt-in, so its survival is asserted rather than assumed. check_out $P/metadata-extra-yaml "Requires-Dist: PyYAML ; extra == 'yaml'" cat "$DI/METADATA" # The RECORD names six entries: one payload module and five dist-info files. check_out $P/record-module "configargparse.py,sha256=" cat "$DI/RECORD" check_eq $P/record-lines "6" "$(grep -c . "$DI/RECORD" 2>/dev/null)" sed -n '/^Package: python3-configargparse$/,/^$/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: 1.7-r0" cat "$W/st" # THE THREE RDEPENDS ARE SUB-PACKAGES OF THE PORTED python3 (BRINTOS-1476) and # are already on this image. The line is asserted EXACTLY, and it is the case # that proves the bbappend did what it says: with the stock `PACKAGECONFIG ?= # "yaml"' this line would carry python3-pyyaml as a fourth entry, and nothing # else in this suite would notice. 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, python3-json, python3-shell" \ "$(cat "$W/dep")" # ...and pyyaml is not on the image at all, which is the same claim from the # rootfs's side rather than from this package's metadata. check $P/no-pyyaml-package test ! -e /var/lib/dpkg/info/python3-pyyaml.list # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import configargparse' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. This module is a plausible source of # one: it still carries a `sys.version_info >= (3, 0)' branch and a # `from StringIO import StringIO' in the dead half of it. check_eq $P/import-quiet "" \ "$(python3 -c 'import configargparse' 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. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "78" "$(pv emitted_count)" # The probe builds its five config files and tears them down. Both are asserted: # a fixture that failed to build makes every ladder case below meaningless, and # one left behind would leak a directory per boot into the guest's tmpfs. check_eq $P/fixture-built "yes" "$(pv fixture_built)" check_eq $P/fixture-torn-down "yes" "$(pv torn_down)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the file # named here. check_eq $P/module-answered "<$MOD>" "$(pv module_file)" check_eq $P/is-flat-module "yes" "$(pv is_flat_module)" # THE MODULE HAS NO `__version__'. The version lives only in the dist-info, so a # caller who wants it at runtime must go through importlib.metadata -- and BOTH # spellings of the distribution answer there, because importlib.metadata # normalises the name. That is not free: the two-name packages beside this one on # the image refuse the spelling they were not installed under. check_eq $P/version-attr "!" "$(pv version_attr)" check_eq $P/version-type "" "$(pv version_type)" check_eq $P/dist-version-mixed "<1.7>" "$(pv dist_version_mixed)" check_eq $P/dist-version-lower "<1.7>" "$(pv dist_version_lower)" # IT IS A DROP-IN REPLACEMENT, WHICH IS A STATEMENT ABOUT ITS NAMESPACE. The # module ends with `from argparse import *', so argparse's public names are # reachable through it -- and `Namespace' is the SAME OBJECT, not a copy, so a # caller mixing the two modules can still isinstance across them. check_eq $P/reexport-namespace "yes" "$(pv reexport_namespace)" check_eq $P/reexport-argumenttypeerror "yes" "$(pv reexport_argumenttypeerror)" check_eq $P/reexport-suppress "yes" "$(pv reexport_suppress)" check_eq $P/reexport-remainder "yes" "$(pv reexport_remainder)" check_eq $P/namespace-is-argparses "yes" "$(pv argparse_is_same_namespace)" check_eq $P/subclasses-argparse "yes" "$(pv argumentparser_subclasses_argparse)" # ------------------------------------------------------ precedence ladder ---- # THE TICKET'S CENTRAL REQUIREMENT, rung by rung. The same option supplied by all # four sources, then by three, then by each pair, then by the default alone -- # with the winner asserted each time. The default is present at every rung by # construction, so `ladder-default-only' is the one-source case. check_eq $P/ladder-all-four "" "$(pv ladder_all_four)" check_eq $P/ladder-env-cfg-default "" "$(pv ladder_env_cfg_default)" check_eq $P/ladder-cfg-default "" "$(pv ladder_cfg_default)" check_eq $P/ladder-default-only "" "$(pv ladder_default_only)" check_eq $P/ladder-cli-and-default "" "$(pv ladder_cli_and_default)" check_eq $P/ladder-cli-and-env "" "$(pv ladder_cli_and_env)" check_eq $P/ladder-cli-and-cfg "" "$(pv ladder_cli_and_cfg)" check_eq $P/ladder-env-and-default "" "$(pv ladder_env_and_default)" # THE EIGHT ABOVE ALL PASS AN EXPLICIT `env_vars' DICT, which is the testing hook # and not what a real caller does. These two are the real path: a variable set on # `os.environ' is picked up, and removing it falls back to the default. A build in # which `env_var' reached os.environ not at all would pass all eight and fail # these. check_eq $P/ladder-real-environ "" "$(pv ladder_real_environ)" check_eq $P/ladder-environ-removed "" "$(pv ladder_environ_removed)" # ------------------------------------------------------ config-file syntax --- # EACH SPELLING THE DEFAULT PARSER SUPPORTS, on its own file. The `eq.cfg' # fixture also opens with all four comment forms the parser accepts -- `#', `;', # an `[ini section]' and a `---' yaml document marker -- so a release that stopped # ignoring any of them turns `syntax-equals' red rather than passing quietly. check_eq $P/syntax-equals "" "$(pv syntax_equals)" check_eq $P/syntax-colon "" "$(pv syntax_colon)" check_eq $P/syntax-space "" "$(pv syntax_space)" # THE BARE FLAG. A line carrying a key and NO separator parses to the string # "true", which `store_true' takes. `syntax-flag-absent' is the control: without # the file the same parser leaves it False, so the case above is about the file # rather than about the action's default. check_eq $P/syntax-bare-flag "" "$(pv syntax_bare_flag)" check_eq $P/syntax-flag-absent "" "$(pv syntax_flag_absent)" # A CONFIG FILE NAMING AN OPTION THE PARSER DOES NOT DEFINE. configargparse does # not decide -- it synthesises `--nosuchoption=7' onto argv and lets argparse # decide -- so the same file is fatal under `parse_args' and a leftover under # `parse_known_args'. The error message names the SYNTHESISED argument and # neither the file nor the line, which is what makes it hard to trace back. check_eq $P/unknown-key-status "<2>" "$(pv unknown_key_parse_args_status)" check_eq $P/unknown-key-stderr \ "" \ "$(pv unknown_key_parse_args_stderr)" check_eq $P/unknown-key-leftover "<--nosuchoption=7>" "$(pv unknown_key_leftover)" # ...and the file's GOOD keys were applied anyway: the file is not discarded # wholesale, which is the half a reader would most likely assume wrong. check_eq $P/unknown-key-others-applied "" \ "$(pv unknown_key_colour_still_parsed)" # A LINE THE PARSER'S OWN REGEX REFUSES becomes a `parser.error()', so the caller # sees an EXIT 2 with a usage message rather than a ConfigFileParserException. # This is the ONE message that names the file and the offending line -- and the # line is numbered from ZERO, so the first line of the file is "line 0". check_eq $P/bad-line-status "<2>" "$(pv bad_line_status)" check_eq $P/bad-line-stderr \ "" \ "$(pv bad_line_stderr)" # A CONFIG FILE THAT IS NOT THERE takes the same exit, from argparse's FileType # rather than from the config parser -- so a typo in a path and a typo in the # file's contents are indistinguishable by exit status alone. check_eq $P/missing-config-status "<2>" "$(pv missing_config_status)" check_eq $P/missing-config-stderr \ "" \ "$(pv missing_config_stderr)" # --------------------------------------------------- parse_known_args -------- # THE TICKET'S REQUIREMENT: unrecognised arguments come back UNTOUCHED, in the # order and the spelling they were given -- including a bare positional, which # this parser declares none of. check_eq $P/pka-leftovers "<--wat 3 positional>" "$(pv pka_leftovers)" check_eq $P/pka-namespace "" "$(pv pka_namespace_colour)" # ...and an empty leftover list is `<>' rather than a missing key, which is the # whole reason every string result here is bracketed. check_eq $P/pka-leftovers-empty "<>" "$(pv pka_leftovers_empty)" # -------------------------------------------------------- format_values ------ # THE TICKET'S "REPORT THE SOURCE OF EACH VALUE" REQUIREMENT, one report per # source, each asserted WHOLE. The layout is the deliverable: the command line is # reported inline after `Command Line Args: ' and the other three open a block # whose entries are indented two spaces and padded to a 19-column key. check_eq $P/fv-cli-wins "" "$(pv fv_cli_wins)" check_eq $P/fv-config-source \ "" \ "$(pv fv_config_source)" check_eq $P/fv-env-source \ "" "$(pv fv_env_source)" check_eq $P/fv-defaults-only \ "" "$(pv fv_defaults_only)" # THE HEADLINE FINDING, ASSERTED. With the option in BOTH the environment and the # config file, the report does not list the file as a losing source -- it does not # list the file at all. The env var is prepended to argv before any config file is # opened, and the config loop discards a key it finds `already_on_command_line'. # A caller reading this report to answer "was my config file used" gets silence. check_eq $P/fv-env-shadows-config \ "" \ "$(pv fv_env_shadows_config)" check_eq $P/stsd-keys "" "$(pv stsd_keys)" # ...and with the environment out of the way the config file IS a source, under a # key that carries the file name after a `|'. That is how `format_values' comes to # name the path, and it is what the case above measures the absence of. check_eq $P/stsd-config-key "" \ "$(pv stsd_config_key)" # --------------------------------------------- --write-out-config-file ------- # IT EXITS 0 AND REPORTS ON STDOUT, not stderr -- a caller piping stdout gets the # notice mixed into their data. `woc-stderr' is the other half of that claim. check_eq $P/woc-status "<0>" "$(pv woc_status)" check_eq $P/woc-stderr "<>" "$(pv woc_stderr)" check_eq $P/woc-stdout "" "$(pv woc_stdout)" # WHAT IT WRITES: the two options that were given, in the default parser's own # `key = value' spelling, and nothing else. check_eq $P/woc-contents "" "$(pv woc_contents)" check_eq $P/woc-has-colour "yes" "$(pv woc_has_colour)" check_eq $P/woc-has-size "yes" "$(pv woc_has_size)" # AN OPTION LEFT AT ITS DEFAULT IS NOT WRITTEN, and neither is the config-file # option nor the write-out option itself -- a file naming either would re-trigger # it on the way back in. check_eq $P/woc-has-verbose "no" "$(pv woc_has_verbose)" check_eq $P/woc-has-config "no" "$(pv woc_has_config)" # FED BACK IN IT YIELDS THE SAME NAMESPACE for every option the file is about... check_eq $P/woc-roundtrip-real-ns \ "<[('colour', 'from-cli'), ('size', 9), ('verbose', False)]>" \ "$(pv woc_roundtrip_real_ns)" check_eq $P/woc-roundtrip-real-equal "yes" "$(pv woc_roundtrip_real_equal)" # ...and the WHOLE namespaces differ, necessarily: `config' is None on the direct # run and holds the file's path on the round trip. Asserted rather than worked # around, so the inequality is a recorded property of the round trip. check_eq $P/woc-roundtrip-ns-equal "no" "$(pv woc_roundtrip_ns_equal)" check_eq $P/woc-roundtrip-ns \ "<[('colour', 'from-cli'), ('config', 'WORKDIR/out.cfg'), ('size', 9), ('verbose', False), ('write_out_config_file', None)]>" \ "$(pv woc_roundtrip_ns)" check_eq $P/woc-direct-ns \ "<[('colour', 'from-cli'), ('config', None), ('size', 9), ('verbose', False), ('write_out_config_file', None)]>" \ "$(pv woc_direct_ns)" # --------------------------------------- a required option, nowhere ---------- # THE TICKET'S LAST REQUIREMENT: the documented status and message when a # `required=True' option is in no source at all. It is argparse's own message, # unchanged -- configargparse adds nothing to it, so nothing tells the caller # that a config file or an environment variable would have satisfied it. check_eq $P/required-missing-status "<2>" "$(pv required_missing_status)" check_eq $P/required-missing-stderr \ "" \ "$(pv required_missing_stderr)" # ...and satisfied from each of the three non-default sources in turn, which is # what says the requirement is checked AFTER the merge rather than against argv. check_eq $P/required-from-cli "" "$(pv required_from_cli)" check_eq $P/required-from-env "" "$(pv required_from_env)" check_eq $P/required-from-config "" "$(pv required_from_config)" # ------------------------------------------------------- the yaml parser ----- # THE ONE BEHAVIOUR THIS PORT REMOVES, asserted from three sides: the class is # still there, the module it wants is not on the image, and the parse refuses # with the message upstream wrote for exactly this case. An image that later # gained python3-pyyaml turns all three red at once and says so, rather than # changing in silence. # # The LAZY import is why declining the extra is safe at all: `_load_yaml' runs # inside `parse'/`serialize', so nothing at module scope touches yaml and # `import configargparse' is unaffected -- which `import-quiet' above measures # from the other end. check_eq $P/yaml-class-exists "yes" "$(pv yaml_class_exists)" check_eq $P/yaml-module-absent "!" "$(pv yaml_import)" check_eq $P/yaml-parse-refuses "!" "$(pv yaml_parse)" check_eq $P/yaml-refusal-message \ "" \ "$(pv yaml_parse_message)" # THE OTHER PARSERS NEED NOTHING THIS IMAGE DOES NOT HAVE, and each is exercised # on its own syntax rather than merely asserted to exist -- "the class is there" # says nothing about whether the module it lazily imports is. # # `TomlConfigParser' lazily imports `toml', which IS on this image, so unlike the # yaml parser this one answers. That is the contrast worth having beside the four # cases above: the mechanism is identical and only the image differs. check_eq $P/cfp-default-parse "<{'a': '1', 'b': '2', 'c': 'true'}>" "$(pv cfp_default_parse)" check_eq $P/cfp-configparser-parse "<{'a': '1'}>" "$(pv cfp_configparser_parse)" check_eq $P/cfp-toml-parse "<{'a': '1'}>" "$(pv cfp_toml_parse)" # THE DEFAULT PARSER'S LIST SYNTAX, both spellings the docstring advertises. The # JSON one goes through `json.loads' -- which is why the recipe declares # python3-json -- and the legacy bare-comma one is what `json.loads' refuses and a # hand-rolled split takes over for. They give the same list. check_eq $P/cfp-list-json "<['apple', 'orange']>" "$(pv cfp_list_json)" check_eq $P/cfp-list-legacy "<['apple', 'orange']>" "$(pv cfp_list_legacy)" # `key =' IS THE EMPTY STRING AND `key' ALONE IS THE STRING "true". The two differ # only in the separator, and that difference is the whole of the bare-flag syntax # `syntax-bare-flag' rests on. check_eq $P/cfp-empty-value "<>" "$(pv cfp_empty_value)" check_eq $P/cfp-no-separator "" "$(pv cfp_no_separator)" rm -rf "$W"