. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # Everything else below reads the filesystem or dpkg with # # shell builtins, or greps a line out of a transcript one # # of those three left behind. # # # # The three are: a bare `import' (`import'), the probe # # (ONE start for 115 measurements, including all 30 # # completions), and one run of the /usr/bin script # # (`register-run'). # # # # 1200 is 400s per start, the budget 99-python3-imagesize # # and 99-python3-configargparse set for their three, and # # roughly six times the ~60s a python3 start has measured # # at on this arm. Nothing here is quadratic in anything, so # # the start is the only thing the number has to cover, and # # both the probe and the script run are additionally bounded # # by `bst_probe' so neither can spend the suite's whole # # budget on its own. # # # # 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-argcomplete 3.3.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-argcomplete_3.3.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHY THAT SHAPES THIS SUITE. # ============================================================================ # # argcomplete gives an `argparse'-based program bash and zsh tab completion. Its # subject is a PROTOCOL CARRIED IN THE ENVIRONMENT, not a terminal interaction: # the shell function argcomplete installs re-runs the program with # `_ARGCOMPLETE=1', `COMP_LINE', `COMP_POINT' and `_ARGCOMPLETE_IFS' set and file # descriptor 8 pointed somewhere it can read; `argcomplete.autocomplete(parser)' # notices `_ARGCOMPLETE', writes the candidate completions to fd 8 separated by # the IFS, and calls `os._exit(0)'. The program's own `main()' never runs. # # So this suite drives it EXACTLY AS A SHELL WOULD and never through one -- no # pty, no expect, no interactive anything. It reaches no device, no peer and no # terminal, and every fixture it uses it writes itself. # # ONE PROCESS FOR ALL THIRTY COMPLETIONS. Because `autocomplete' EXITS, the # obvious shape -- one process per completion -- is thirty python3 starts, and a # start on this arm is tens of seconds (BRINTOS-1476, brintos/linux#21). The # probe instead dup2's a real pipe onto fd 8 (so argcomplete opens descriptor 8 # itself, as it does under bash), sets the four environment variables per case # and removes them afterwards, and passes an `exit_method' that RAISES. That is # the single substitution: it is a documented parameter of # `CompletionFinder.__call__', and argcomplete reaches it only after # `output_stream.write()' and `.flush()' have already run, so nothing about what # the caller reads is changed by it. # # NO fork(2) ON THE DEFAULT PATH. `FilesCompleter' -- the DEFAULT completer for # an option with no completer of its own -- shells out to `bash -c compgen' # through `subprocess.check_output'. That is a platform question rather than an # argcomplete one and it is minutes here, so the probe's parser gives every # value-taking option an explicit completer and the one `FilesCompleter' call is # opt-in behind `BST_AC_FILES_COMPLETER=1'; its keys are absent otherwise and the # cases below skip rather than red. `DirectoriesCompleter' IS exercised: it is # `os.listdir', not compgen. # # ============================================================================ # THE PORT IS ONE IMAGE_INSTALL LINE, THIS SUITE, ITS PROBE -- AND ONE bbappend. # ============================================================================ # # No patch and no site answer: 2318 lines of Python across ten modules, no # configure step, no compile step, no C. MEASURED rather than read off the # recipe -- every .py the package installs is BYTE-IDENTICAL to the sdist the # SRC_URI sha256 pins. Recipe-scope build: 1603 of 1603 tasks, all succeeded, # zero ERROR: lines. # # THE bbappend ADDS ONE RDEPENDS: python3-shell. The stock recipe declares # `python3-core python3-io', and that is not enough to import the package at all. # `argcomplete/__init__.py' imports `.shell_integration' unconditionally, and # `shell_integration.py' line 6 is `from shlex import quote' -- module scope, not # lazy -- while poky's python3-manifest.json puts `shlex.py' in python3-SHELL. # Measured on an x86-64 control with a `sys.meta_path' hook refusing `shlex': # all seven importable names raise `ModuleNotFoundError: No module named # 'shlex''. There is no partial degradation, because the failure is in the # top-level `__init__' every submodule import runs first. `deb-depends-exact' # pins the corrected line, so the claim is re-measured on every image. # # ============================================================================ # THE HEADLINE FINDING: THE DISTRIBUTION VERSION ON THIS IMAGE IS 0.0.0. # ============================================================================ # # The recipe is `python3-argcomplete_3.3.0.bb', the .deb is `3.3.0-r0', and the # sdist's own PKG-INFO says `Version: 3.3.0'. The dist-info directory Yocto # installs is `argcomplete-0.0.0.dist-info' and its METADATA says # `Version: 0.0.0', so `importlib.metadata.version("argcomplete")' answers # "0.0.0" ON TARGET (`dist-version', measured against the built .deb's tree). # # The cause is upstream's `dynamic = ["version"]' with `setuptools_scm': the # wheel build re-derives the version from git, there is no git in the unpacked # sdist, and setuptools_scm falls back to 0.0.0 rather than reading the PKG-INFO # the sdist ships. The recipe never notices, because it takes PV from its own # filename. # # WHY IT MATTERS RATHER THAN BEING A COSMETIC WART. argcomplete has no # `__version__' attribute at all (`version-attr' is `!'), so # `importlib.metadata' is the ONLY way to ask, and it is the way a caller # actually asks -- a `packaging.version.parse(version("argcomplete")) >= 3' # feature guard is the documented idiom for the 2.x/3.x split. That guard reads # 0.0.0 here and takes the pre-2.0 branch of a package that is 3.3.0. # `dist-name-normalises' shows the lookup itself works, so the failure is a wrong # ANSWER, not a missing one, and nothing on the image says so. # # ============================================================================ # SECOND FINDING: `register-python-argcomplete --shell zsh' IS THE BASH FORM. # ============================================================================ # # BRINTOS-2209 asks for `--shell zsh' to be asserted to DIFFER from the bash # form. It does not. `shell_integration.shellcode' takes ONE branch for both -- # `if shell == "bash" or shell == "zsh"' -- and fills in a template that does not # mention the shell, so the two are byte identical (`shellcode-zsh-identical', # and 2658 bytes each). The emitted script decides at RUNTIME, on `$ZSH_VERSION', # which half of itself to use; `#compdef' on line 1 is what zsh reads and bash # ignores. # # So a caller diffing the two outputs to find out what zsh support looks like # finds nothing, and `--shell zsh' cannot be used to check whether a build has # zsh support -- it always succeeds. tcsh, fish and powershell DO get their own # text (`shellcode-tcsh', `shellcode-fish-function'), which is what makes the # bash/zsh case surprising rather than obviously deliberate. # # AND THE tcsh TEXT NAMES A HELPER THIS IMAGE DOES NOT HAVE. It registers # `python-argcomplete-tcsh', and the package ships three scripts, none of them # that one (`no-tcsh-helper'). Upstream dropped it in 3.x and left the # registration text behind, so tcsh registration here succeeds and completes # nothing. # # ============================================================================ # THIRD FINDING: THE `validator' REACHES COMPLETER OUTPUT ONLY. # ============================================================================ # # `CompletionFinder(validator=...)' is documented as "function to filter ALL # completions through before returning". It is applied in exactly one place -- # the loop over a completer's output -- so option strings and SUBCOMMAND NAMES # never see it. Three cases measure the asymmetry on one parser: # `validator-choices' refuses everything and gets nothing (those came from a # ChoicesCompleter), while `validator-options' refuses everything and still gets # all eight option strings, and `validator' asks for subcommands starting "ba" # and gets `build' as well as `bake'. # # A caller who supplies a validator to keep something out of a user's sight -- # a hidden subcommand, a deprecated flag -- is not doing it. `exclude=' is, and # `filter-exclude' and `comp-exclude' assert that it works; the two knobs read # as alternatives in the documentation and only one of them covers option names. # # ============================================================================ # SMALLER THINGS THE CASES BELOW STATE THAT A CALLER WOULD GET WRONG. # ============================================================================ # # comp-partial-long A UNIQUE completion comes back with a TRAILING # SPACE -- `--colour ', not `--colour'. That is # `append_space', on by default, and it is why a # caller comparing argcomplete's output to a literal # gets a mismatch on exactly the single-candidate # case. `comp-no-space' is the same line with # `append_space=False'. # comp-nomatch A prefix matching nothing is the EMPTY STRING and # `exit(0)' -- not an error, not a diagnostic. The # shell cannot tell "no candidates" from "the # program is not argcomplete-aware". # comp-zsh The zsh wire format is `completion:description', # and a subparser with no `help=' yields the literal # string `None' -- `build:None'. A zsh user sees the # word None in the menu. # ifs-invalid-* An `_ARGCOMPLETE_IFS' that is not exactly one # character makes argcomplete exit 1 having written # NOTHING, so the shell gets silence rather than a # wrong answer. # inactive-* With `_ARGCOMPLETE' unset, `autocomplete()' # RETURNS and calls no exit method. That is the # whole reason a program may call it unconditionally # at the top of main(). # comp-after-fast* `CompletionFinder' offers `--slow' after `--fast' # has been typed even though argparse would refuse # the pair; `ExclusiveCompletionFinder' is the # subclass that drops it. Same line, both finders. # comp-equals `--colour=g' completes to `--colour=green ' -- the # option is re-attached, so the shell replaces the # whole word. # shebang-* Upstream ships two of its three scripts with # `#!/usr/bin/env python'. There is no # /usr/bin/python on this image, so they would be # unrunnable; Yocto's shebang mangling rewrote both # to python3, and these cases pin that rather than # trusting it. # # ============================================================================ # ON TARGET: 114 probe measurements over ONE python3 start. # ============================================================================ # # EVERY STRING RESULT IS WRAPPED IN ANGLE BRACKETS and the empty string is `<>'. # Load-bearing here: "the completion of a prefix matching nothing" is # legitimately the empty string, and `grep | cut' cannot otherwise tell an empty # ANSWER from a key the probe never reached. A refusal is `!'. # # A COMPLETION LIST IS JOINED WITH `|' after the transport, so the literals below # read as lists rather than as \013-separated blobs. The transport separator # itself is asserted separately, by `ifs-default-seen' and `ifs-custom'. # # THE PROBE'S OWN TEMPORARY DIRECTORY IS REPLACED BY THE LITERAL `WORKDIR', # because `mkdtemp' picks a fresh suffix every boot and the path appears inside # the directory completions. # # ============================================================================ P=python3-argcomplete # 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 PKGDIR=$SITE/argcomplete DI=$SITE/argcomplete-0.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-argcomplete.list PROBE=$BST_DATA/python3-argcomplete-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-argcomplete.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A PACKAGE WITH A SUB-PACKAGE, not a flat module: ten .py files under # `argcomplete/' plus two under `argcomplete/packages/', which are vendored # copies of argparse and shlex internals rather than imports of them. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-flat-module test ! -e "$SITE/argcomplete.py" check $P/init test -s "$PKGDIR/__init__.py" check $P/finders test -s "$PKGDIR/finders.py" check $P/completers test -s "$PKGDIR/completers.py" check $P/lexers test -s "$PKGDIR/lexers.py" check $P/io test -s "$PKGDIR/io.py" check $P/exceptions test -s "$PKGDIR/exceptions.py" check $P/shell-integration test -s "$PKGDIR/shell_integration.py" check $P/check-module test -s "$PKGDIR/_check_module.py" check $P/check-console test -s "$PKGDIR/_check_console_script.py" check $P/packages-dir test -d "$PKGDIR/packages" check $P/packages-argparse test -s "$PKGDIR/packages/_argparse.py" check $P/packages-shlex test -s "$PKGDIR/packages/_shlex.py" # THE TYPE MARKER AND THE BASH COMPLETION FRAGMENT ARE DATA FILES, shipped # through `package_data' rather than as code. `py.typed' is what makes the # annotations in finders.py visible to a checker at all, and it is EMPTY, so # `test -s' would fail on it -- the one place in this suite where `test -f' is # the right assertion rather than a weaker one. check $P/py-typed test -f "$PKGDIR/py.typed" check $P/py-typed-empty test ! -s "$PKGDIR/py.typed" check $P/bash-completion-d test -s "$PKGDIR/bash_completion.d/_python-argcomplete" # The .pyc files ship in the .deb, which is what lets the package import on a # READ-ONLY root: nothing has to be compiled at first import. check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-finders test -s "$PKGDIR/__pycache__/finders.cpython-312.pyc" check $P/pycache-packages test -s "$PKGDIR/packages/__pycache__/_shlex.cpython-312.pyc" # THE THREE CONSOLE SCRIPTS. `scripts=glob.glob("scripts/*")' in setup.py, so # these are plain files copied into ${bindir} rather than generated entry points. check $P/bin-register test -x /usr/bin/register-python-argcomplete check $P/bin-check-easy test -x /usr/bin/python-argcomplete-check-easy-install-script check $P/bin-activate test -x /usr/bin/activate-global-python-argcomplete # ...AND THE ONE THE tcsh REGISTRATION NAMES IS NOT AMONG THEM. See the second # finding above: `shellcode(shell="tcsh")' registers `python-argcomplete-tcsh', # which upstream dropped and this package does not ship, so tcsh registration # succeeds and completes nothing. check $P/no-tcsh-helper test ! -e /usr/bin/python-argcomplete-tcsh # UPSTREAM SHIPS TWO OF THE THREE WITH `#!/usr/bin/env python' and there is no # /usr/bin/python on this image. Yocto's shebang mangling rewrote both; these # pin the rewrite rather than trusting it, and `no-usr-bin-python' says why it # had to happen. check_eq $P/shebang-register "#!/usr/bin/env python3" \ "$(head -n 1 /usr/bin/register-python-argcomplete)" check_eq $P/shebang-check-easy "#!/usr/bin/env python3" \ "$(head -n 1 /usr/bin/python-argcomplete-check-easy-install-script)" check_eq $P/shebang-activate "#!/usr/bin/env python3" \ "$(head -n 1 /usr/bin/activate-global-python-argcomplete)" check $P/no-usr-bin-python test ! -e /usr/bin/python # THE MARKER COMMENT SURVIVES. `# PYTHON_ARGCOMPLETE_OK' on line 2 of a script is # how `python-argcomplete-check-easy-install-script' recognises an # argcomplete-aware program, and it is a COMMENT -- exactly the kind of thing a # packaging step that rewrites shebangs could drop. check_eq $P/marker-register "# PYTHON_ARGCOMPLETE_OK" \ "$(sed -n 2p /usr/bin/register-python-argcomplete)" # ------------------------------------------------------------- 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. # argcomplete 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, the two data files and the three scripts in the # payload. A file appearing here that is none of those is 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 ;; */argcomplete/py.typed) continue ;; */bash_completion.d/*) continue ;; /usr/bin/*python*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # TEN MODULES UNDER argcomplete/ AND TWO UNDER packages/. A release that split or # merged a module would show up here first, before any behavioural case noticed. check_eq $P/pkg-py-count "12" \ "$(grep -c '/site-packages/argcomplete/.*\.py$' "$W/pkgf" 2>/dev/null)" # UPSTREAM'S OWN test/ TREE IS NOT ON THE ROOTFS. setup.py says # `find_packages(exclude=["test", "test.*"])', so the sdist ships it and the # wheel does not. Several packages beside this one on the image DO ship their # tests; the difference is a packaging decision rather than a property of the # source. check $P/no-tests-dir test ! -d "$SITE/test" # ------------------------------------------------------- the dist-info ------- # NAMED 0.0.0, WHICH IS THE HEADLINE FINDING. Asserted by DIRECTORY NAME as well # as by METADATA, because those are two separate consequences of the same cause # and a recipe that fixed one without the other would still mislead # importlib.metadata. 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.0.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: argcomplete" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/no-3-3-0-dist-info test ! -d "$SITE/argcomplete-3.3.0.dist-info" # `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" check_eq $P/dist-info-no-py2-tag "" "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check_eq $P/dist-info-top-level-name "argcomplete" "$(cat "$DI/top_level.txt" 2>/dev/null)" check $P/dist-info-flat-license test -s "$DI/LICENSE.rst" check $P/dist-info-notice test -s "$DI/NOTICE" check $P/dist-info-no-licenses-dir test ! -d "$DI/licenses" # THE ONLY Requires-Dist ENTRIES ARE THE `test' EXTRA -- five of them, none of # them a runtime dependency. That is what makes the RDEPENDS question purely # one about the STDLIB SPLIT rather than about third-party packages, and it is # why the bbappend adds python3-shell and nothing else. check_eq $P/metadata-requires-count "5" \ "$(grep -c '^Requires-Dist:' "$DI/METADATA" 2>/dev/null)" check_eq $P/metadata-no-runtime-dep "" \ "$(grep '^Requires-Dist:' "$DI/METADATA" 2>/dev/null | grep -v "extra == 'test'")" check_out $P/metadata-requires-python "Requires-Python: >=3.8" cat "$DI/METADATA" sed -n '/^Package: python3-argcomplete$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/deb-known test -s "$W/st" check_out $P/deb-installed "Status: install ok installed" cat "$W/st" # THE dpkg VERSION IS RIGHT WHILE THE PYTHON ONE IS NOT. Both are asserted, next # to each other, because that pair IS the finding: nothing an image-level tool # reads is wrong, and everything a python caller reads is. check_out $P/deb-version "Version: 3.3.0-r0" cat "$W/st" # THE RDEPENDS LINE, EXACTLY -- and this is the case that proves the bbappend did # what it says. With the stock recipe this reads # "Depends: python3-core, python3-io" and `import argcomplete' fails on any # image without python3-shell, with nothing else in this suite noticing. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/deb-depends-line test -s "$W/dep" check_eq $P/deb-depends-exact "Depends: python3-core, python3-io, python3-shell" \ "$(cat "$W/dep")" # ...and shlex.py, the file that whole line is about, is on the rootfs. check $P/shlex-present test -s /usr/lib/python3.12/shlex.py # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import argcomplete' # ----------------------------------------------------------------- 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 IS ALSO THIS SUITE'S `import-quiet'. It imports argcomplete as its # first act and nothing below it writes to stderr on a clean run, so an empty # stderr is a statement about the import as well as about the probe. check_eq $P/probe-quiet "" "$(cat "$OUT".err 2>/dev/null)" # 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' reads as a wrong answer rather than # as an absent one. check_eq $P/probe-complete "" "$(pv probe_done)" check_eq $P/probe-count "114" "$(pv emitted_count)" check_eq $P/fixture-built "" "$(pv fixture_built)" check_eq $P/fixture-torn-down "" "$(pv torn_down)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # file named here. check_eq $P/module-answered "<$PKGDIR/__init__.py>" "$(pv module_file)" check_eq $P/is-package "" "$(pv is_package)" # THE HEADLINE PAIR. There is no `__version__' attribute, so importlib.metadata # is the only way to ask -- and it answers 0.0.0 for a 3.3.0 package. See the # top of this file for why that reaches a caller rather than staying cosmetic. check_eq $P/version-attr "!" "$(pv version_attr)" check_eq $P/dist-version "<0.0.0>" "$(pv dist_version)" check_eq $P/submodule-completers "" "$(pv submodule_completers)" check_eq $P/submodule-exceptions "" "$(pv submodule_exceptions)" check_eq $P/submodule-finders "" "$(pv submodule_finders)" check_eq $P/submodule-io "" "$(pv submodule_io)" check_eq $P/submodule-lexers "" "$(pv submodule_lexers)" check_eq $P/submodule-shell-integration "" "$(pv submodule_shell_integration)" check_eq $P/submodule-packages-argparse "" "$(pv submodule_packages__argparse)" check_eq $P/submodule-packages-shlex "" "$(pv submodule_packages__shlex)" # `argcomplete.autocomplete' IS AN INSTANCE, not a function -- a module-level # `CompletionFinder()' whose `__call__' re-runs its own `__init__' on every # invocation. That is why the probe can drive thirty completions through the one # object without them contaminating each other. check_eq $P/autocomplete-type "" "$(pv autocomplete_type)" check_eq $P/autocomplete-is-finder "" "$(pv autocomplete_is_finder)" # ------------------------------------------------- the completion protocol --- # THE TICKET'S SIX SHAPES, in its own order. The cursor is at end of line in # every one of them, which is what a shell reports for a tab pressed at the end # of what has been typed. Each carries its exit call as a second case, because # "argcomplete answered" and "argcomplete exited 0 having answered" are separate # claims and only the second is what the shell function is waiting on. # A BARE PROGRAM NAME OFFERS EVERYTHING: both spellings of every option and all # three subcommands. Short options are included because `always_complete_options' # defaults to True. check_eq $P/comp-bare \ "<-h|--help|-c|--colour|-v|--verbose|-o|--output|--dir|--plain|--fast|--slow|build|bake|clean>" \ "$(pv comp_bare)" check_eq $P/comp-bare-exit "" "$(pv comp_bare_exit)" # A PARTIAL LONG OPTION WITH ONE CANDIDATE COMES BACK WITH A TRAILING SPACE. # `append_space' is on by default; see `comp-no-space' for the same line without # it. This is the case a caller most often asserts wrongly. check_eq $P/comp-partial-long "<--colour >" "$(pv comp_partial_long)" check_eq $P/comp-partial-long-exit "" "$(pv comp_partial_long_exit)" # A PARTIAL SUBCOMMAND OFFERS ONLY SUBCOMMANDS -- no options, because the prefix # `b' does not start with a prefix char, so `_include_options' returns none. check_eq $P/comp-partial-sub "" "$(pv comp_partial_sub)" check_eq $P/comp-partial-sub-exit "" "$(pv comp_partial_sub_exit)" # AN OPTION WITH `choices=' COMPLETES TO ITS CHOICES, IN DECLARATION ORDER -- # not sorted. argcomplete synthesises a ChoicesCompleter for it; the parser # never had one. check_eq $P/comp-choices "" "$(pv comp_choices)" check_eq $P/comp-choices-exit "" "$(pv comp_choices_exit)" # A COMPLETED OPTION EXPECTING A VALUE reaches that option's own completer. check_eq $P/comp-optval "" "$(pv comp_optval)" check_eq $P/comp-optval-exit "" "$(pv comp_optval_exit)" # A PREFIX MATCHING NOTHING IS THE EMPTY STRING AND exit(0) -- no error, no # diagnostic. The shell cannot distinguish this from a program that is not # argcomplete-aware at all. check_eq $P/comp-nomatch "<>" "$(pv comp_nomatch)" check_eq $P/comp-nomatch-exit "" "$(pv comp_nomatch_exit)" # ...and the shapes around them that a caller meets on the first day. check_eq $P/comp-partial-short \ "<-h|--help|-c|--colour|-v|--verbose|-o|--output|--dir|--plain|--fast|--slow>" \ "$(pv comp_partial_short)" check_eq $P/comp-choices-partial "" "$(pv comp_choices_partial)" # `--colour=g' COMES BACK WITH THE OPTION RE-ATTACHED, because the shell replaces # the whole word rather than the part after the `='. check_eq $P/comp-equals "<--colour=green >" "$(pv comp_equals)" # INSIDE A SUBCOMMAND the parent's options are gone and the subparser's are # offered -- including its own `--help', which is a different action object. check_eq $P/comp-sub-options "<--help|--target>" "$(pv comp_sub_options)" check_eq $P/comp-sub-choices "" "$(pv comp_sub_choices)" check_eq $P/comp-after-sub "<-h|--help|--target>" "$(pv comp_after_sub)" # A DIRECTORY COMPLETER. `DirectoriesCompleter' is `os.listdir' and a predicate, # with NO subprocess -- which is why it is on the default path here and # `FilesCompleter', which shells out to `bash -c compgen', is not. The # completions carry the full prefix and a trailing slash. check_eq $P/comp-dirs "" "$(pv comp_dirs)" # `SuppressCompleter' HIDES THE OPTION'S VALUES, NOT THE OPTION ITSELF. # `comp-suppressed' completes the option NAME `--sec' and gets nothing, and # `comp-secret-value' asks for its values and gets nothing -- while # `comp-print-suppressed' shows the name coming back as soon as # `print_suppressed=True' is passed. So the marker is a display decision made at # completion time, not a property of the parser. check_eq $P/comp-suppressed "<>" "$(pv comp_suppressed)" check_eq $P/comp-secret-value "<>" "$(pv comp_secret_value)" check_eq $P/comp-print-suppressed "<--secret >" "$(pv comp_print_suppressed)" # THE CURSOR IS NOT THE END OF THE LINE. COMP_POINT is honoured: everything # after it is the suffix and is not completed. check_eq $P/comp-midline "<--verbose >" "$(pv comp_midline)" # `exclude=' REMOVES BOTH SPELLINGS BY NAME, which is the knob that does what a # caller reaching for `validator=' usually meant. See the third finding above. check_eq $P/comp-exclude "<>" "$(pv comp_exclude)" # `always_complete_options' HAS THREE VALUES, not two: False drops option strings # entirely, and "long" keeps the long spelling of every option plus any short # option that has no long variant -- here that is none, so the short forms all # vanish. check_eq $P/comp-no-options "" "$(pv comp_no_options)" check_eq $P/comp-long-only \ "<--help|--colour|--verbose|--output|--dir|--plain|--fast|--slow|build|bake|clean>" \ "$(pv comp_long_only)" check_eq $P/comp-no-space "<--colour>" "$(pv comp_no_space)" # THE THREE VALIDATOR CASES, which are one finding: the validator reaches # completer output only. `validator' refuses everything but "ba*" and still gets # `build'; `validator-options' refuses EVERYTHING and still gets all eight # option strings; `validator-choices' refuses everything and gets nothing, # because those came from a ChoicesCompleter. check_eq $P/comp-validator "" "$(pv comp_validator)" check_eq $P/comp-validator-options \ "<--help|--colour|--verbose|--output|--dir|--plain|--fast|--slow>" \ "$(pv comp_validator_options)" check_eq $P/comp-validator-choices "<>" "$(pv comp_validator_choices)" # `_ARGCOMPLETE' IS NOT A BOOLEAN. It is 1 for `prog args', 2 for # `python prog args' and 3 for `python -m mod args', and it says how many words # to drop off the front -- so the same COMP_LINE completes differently under a # different value. check_eq $P/comp-start2 "<--colour >" "$(pv comp_start2)" # THE ZSH WIRE FORMAT is `completion:description', and a subparser declared with # no `help=' yields the literal string `None'. A zsh user sees the word None in # the completion menu; nothing upstream converts it to an empty description. check_eq $P/comp-zsh "" "$(pv comp_zsh)" # THE SEPARATOR IS THE SHELL'S TO CHOOSE. The default is \013; a caller who sets # another gets it back, and the default no longer appears. check_eq $P/ifs-default-seen "" "$(pv ifs_default_sep_seen)" check_eq $P/ifs-custom "" "$(pv ifs_custom)" check_eq $P/ifs-custom-no-vt "" "$(pv ifs_custom_has_vt)" # AN IFS THAT IS NOT ONE CHARACTER IS A REFUSAL: exit 1, nothing written. The # shell gets silence rather than a wrongly-split answer. check_eq $P/ifs-invalid-exit "" "$(pv ifs_invalid_exit)" check_eq $P/ifs-invalid-out "<>" "$(pv ifs_invalid_out)" # WITH `_ARGCOMPLETE' UNSET, autocomplete() RETURNS and calls no exit method. # That is the whole reason a program may call it unconditionally at the top of # main() -- and it is the case that would break every argcomplete-aware program # on the image at once if it regressed, without any completion being involved. check_eq $P/inactive-returns "" "$(pv inactive_returns)" check_eq $P/inactive-no-exit "" "$(pv inactive_exit_called)" # THE TWO FINDERS ON THE SAME LINE. `--fast' and `--slow' are in a mutually # exclusive group, so argparse would refuse the pair -- and the default finder # offers `--slow' anyway. `ExclusiveCompletionFinder' is the subclass that drops # every option already seen, which is why both `--fast' and `--slow' are gone # from its answer. check_eq $P/comp-after-fast \ "<--help|--colour|--verbose|--output|--dir|--plain|--fast>" "$(pv comp_after_fast)" check_eq $P/comp-after-fast-exclusive \ "<--help|--colour|--verbose|--output|--dir|--plain>" "$(pv comp_after_fast_exclusive)" # EVERY COMPLETION EXITED THE WAY THE SHELL FUNCTION IS WAITING ON. The thirty # `*_exit' keys are asserted in aggregate rather than one case each: 28 of them # are `exit:0' and the only other one is the deliberate `ifs-invalid' refusal. # Stated as a count plus an emptiness, so a case that started RETURNING instead # of exiting -- which is what a broken `exit_method' contract looks like, and # which no individual completion's TEXT would notice -- reds here. check_eq $P/comp-exits-zero "28" "$(grep -c '_exit=$' "$OUT" 2>/dev/null)" check_eq $P/comp-exits-other "" \ "$(grep '_exit=<' "$OUT" 2>/dev/null | grep -v '_exit=$' | grep -v '^ifs_invalid_exit=')" # ------------------------------------------------------- the API, directly --- # `filter_completions' de-duplicates PRESERVING FIRST-SEEN ORDER and applies # `exclude'. It is the ticket's named entry point and it is exposed for # subclassing, so its contract -- a list, order preserved, duplicates dropped -- # is asserted directly rather than only through a completion. check_eq $P/filter-dedup "" "$(pv filter_dedup)" check_eq $P/filter-empty "<>" "$(pv filter_empty)" check_eq $P/filter-exclude "<--colour>" "$(pv filter_exclude)" check_eq $P/filter-returns-list "" "$(pv filter_returns_list)" # `SuppressCompleter' IS A MARKER, NOT A COMPLETER. It subclasses BaseCompleter # and answers `suppress() -> True', and it has no `__call__' of its own -- so # calling it reaches BaseCompleter's and raises NotImplementedError. A caller who # treats it as a completer gets an exception rather than an empty completion. check_eq $P/suppress-is-base "" "$(pv suppress_is_base)" check_eq $P/suppress-suppress "" "$(pv suppress_suppress)" check_eq $P/suppress-called-raises "" "$(pv suppress_called_raises)" # `ChoicesCompleter' STRINGIFIES whatever it is given, so a parser built from a # list of ints or an enum still completes. check_eq $P/choices-str "" "$(pv choices_str)" check_eq $P/choices-nonstr "<1|2.5|None>" "$(pv choices_nonstr)" check_eq $P/dirs-completer "" "$(pv dirs_completer)" # `split_line' IS THE LEXER THE WHOLE PROTOCOL RESTS ON: COMP_LINE plus # COMP_POINT in, and (prequote, prefix, suffix, words, wordbreak) out. The values # are joined with `|', so an empty field reads as two adjacent pipes. check_eq $P/split-plain "<|--co||bstprog|>" "$(pv split_plain)" check_eq $P/split-quoted "<'|bu||bstprog|>" "$(pv split_quoted)" check_eq $P/split-midpoint "<|--c||bstprog|>" "$(pv split_midpoint)" check_eq $P/split-wordbreak "<|a:b||bstprog|>" "$(pv split_wordbreak)" # ------------------------------------------------------------- shellcode ----- # `register-python-argcomplete' is a 40-line argparse wrapper whose whole body is # `print(shellcode(...))'. These measure the text for free, in the probe's # process; `register-run' below runs the actual /usr/bin script once and the two # are asserted to agree. check_eq $P/shellcode-head "<#compdef bstprog>" "$(pv shellcode_bash_head)" check_eq $P/shellcode-names-prog "" "$(pv shellcode_names_prog)" check_eq $P/shellcode-function "" "$(pv shellcode_function)" check_eq $P/shellcode-complete-line \ "" \ "$(pv shellcode_complete_line)" check_eq $P/shellcode-compdef-line \ "" "$(pv shellcode_compdef_line)" check_eq $P/shellcode-no-defaults \ "" \ "$(pv shellcode_no_defaults_line)" check_eq $P/shellcode-own-args \ "" \ "$(pv shellcode_own_args_line)" # THE SECOND FINDING, ASSERTED. bash and zsh get the SAME 2658 bytes; the script # branches at runtime on $ZSH_VERSION. The two lengths are asserted separately # from the equality so that a release which changes the template says so once # rather than turning the equality into a mystery. check_eq $P/shellcode-zsh-identical "" "$(pv shellcode_zsh_identical)" check_eq $P/shellcode-bash-len "<2658>" "$(pv shellcode_bash_len)" check_eq $P/shellcode-zsh-len "<2658>" "$(pv shellcode_zsh_len)" # ...WHILE tcsh AND fish GENUINELY DIFFER, which is what makes the bash/zsh case # surprising rather than obviously deliberate. The tcsh line names # `python-argcomplete-tcsh', which `no-tcsh-helper' above shows is not on this # image. check_eq $P/shellcode-tcsh \ "" \ "$(pv shellcode_tcsh)" check_eq $P/shellcode-fish-differs "" "$(pv shellcode_fish_differs)" check_eq $P/shellcode-fish-function "" \ "$(pv shellcode_fish_function)" # AN UNKNOWN SHELL IS THE EMPTY STRING, NOT AN ERROR. `shellcode(shell="zsh4")' # returns "" and `register-python-argcomplete --shell' has no choices= on it, so # a typo registers nothing and reports success. That is the same silence # `comp-nomatch' has, one layer up. check_eq $P/shellcode-unknown-shell-empty "" "$(pv shellcode_unknown_shell_empty)" check_eq $P/shellcode-two-progs \ "" \ "$(pv shellcode_two_progs)" check_eq $P/shellcode-external-script "" "$(pv shellcode_external_script)" check_eq $P/shellcode-external-suffix "" "$(pv shellcode_external_suffix)" # `FilesCompleter' IS THE DEFAULT COMPLETER, which is the fact that made the # no-subprocess rule necessary: any value-taking option without a completer of # its own reaches `bash -c compgen'. check_eq $P/files-completer-is-default "" "$(pv files_completer_is_default)" # THE OPT-IN CASE. Absent unless BST_AC_FILES_COMPLETER=1, and skipped rather # than red when absent -- see the header for why fork(2) is off the default path. if [ "$(pv files_completer_deferred)" = "" ]; then check_eq $P/files-completer "" "$(pv files_completer)" else bst_skip $P/files-completer "BST_AC_FILES_COMPLETER unset: FilesCompleter shells out to bash -c compgen, which is fork(2)+exec(2) and minutes on this arm" fi # ------------------------------------------------- the /usr/bin script run --- # THE THIRD AND LAST python3 START. Everything above measured `shellcode()' in # the probe's own process; this proves the SHIPPED SCRIPT reaches it -- the # shebang resolves, the package imports from an installed path, argparse runs, # and the text comes out on stdout. Bounded by `bst_probe' so a start that never # returns costs this case rather than the suite's whole budget. bst_probe $P/register-run "$W/reg" 400 register-python-argcomplete bstprog if [ -s "$W/reg" ]; then bst_ok $P/register-output check_eq $P/register-head "#compdef bstprog" "$(head -n 1 "$W/reg")" check_eq $P/register-complete-line \ "complete -o nospace -o default -o bashdefault -F _python_argcomplete bstprog" \ "$(grep -E '^[[:space:]]*complete -o' "$W/reg" | head -n 1 | sed 's/^[[:space:]]*//')" # THE SCRIPT AND THE LIBRARY AGREE BYTE FOR BYTE, which is what says the # 2658-byte figure above is about the text a user actually gets. check_eq $P/register-agrees-with-shellcode "2658" "$(wc -c < "$W/reg" | tr -d ' ')" check_eq $P/register-quiet "" "$(cat "$W/reg".err 2>/dev/null)" else bst_notok $P/register-output "register-python-argcomplete produced no output" fi rm -rf "$W"