. "$BST_LIB" # BST-TIMEOUT: 1800 # TEN python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those ten. # # # # Three are the module: a bare `import', a second one whose # # STDERR is the assertion (`import-quiet'), and the probe, # # which takes ONE start for eighty-odd measurements. The # # other SEVEN run a SHIPPED EXAMPLE PROGRAM as a real # # process, and they cannot be folded into the probe -- the # # point of them is what a caller's SHELL sees: the printed # # text on the right stream and the exit STATUS, neither of # # which is observable from inside the interpreter that is # # exiting. Nor can they be folded into each other: a case # # asserting stdout cannot also assert the exit status, # # because `$( )' yields the text and `check_rc' yields the # # status, so each of the three exiting paths costs two. # # # # The work inside each start is a handful of `re' calls over # # usage strings under 400 bytes -- microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 1800 is 180s per start, the same budget per start that # # 99-python3-smartypants sets for its own ten, and three # # times the ~60s a python3 start has measured at on this # # arm. The start dominates and nothing here is quadratic in # # anything. # # # # A suite-timeout red is a batch-wide cost, so if the six # # 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-docopt 0.6.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-docopt_0.6.2.bb, `inherit setuptools3 pypi', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES: THE DOCSTRING IS THE GRAMMAR. # ============================================================================ # # Every other argument parser has you BUILD a parser and then GENERATE a help # message from it. docopt inverts that: you write the help message, and the help # message IS the parser. `docopt(doc, argv)' reads the usage patterns out of # `doc', matches `argv' against them, and returns a dictionary keyed by the # elements the usage named -- `--verbose', `', `FILE', or a bare command # word. # # That inversion is what this suite has to assert, and it is why almost every # case below is a (usage string, argv) pair compared against a WHOLE dictionary # rather than against one extracted field. A parser that got one key right and # dropped another would pass a spot check; comparing the dictionary whole is # what makes each case a statement about the parse rather than about a lookup. # # ============================================================================ # THE TEN CONSTRUCTS, AND WHY EACH IS ASSERTED BOTH WAYS. # ============================================================================ # # The usage grammar defines a short option, a long option, an option taking an # argument, a repeated option (counted), a positional argument, an optional # group in square brackets, a required group in parentheses, mutual exclusion # with a pipe, `...' for repetition, and the `[options]' shortcut. Each has a # case for the argv that USES it and a case for the argv that does not, because # a parser returning a constant dictionary would pass either half alone. # # The absent halves are the ones worth reading. docopt's dictionary always # carries EVERY element the usage names -- that is the property that lets a # caller index it without a guard -- and the value it carries when the element # was not given is not one thing: # # a flag absent -> False # an option-with-argument -> None (it has a value slot; there is no value) # a positional, optional -> None # a repeatable, absent -> [] or the default split on whitespace # a counted flag -> an INTEGER, 0 upward # # `counted-*' is the case the probe's canonical form was built for: `-v' coming # back as `3' and `--num' coming back as `'3'' print identically under `str', # and they are different contracts. Every value below is emitted through `repr', # so an integer and its decimal string cannot collapse into the same text. # # ============================================================================ # THE FAILURE PATHS ARE THE PART A CALLER WRITES CODE AGAINST. # ============================================================================ # # `DocoptExit' -- what a rejected argv raises -- DERIVES FROM SystemExit. So a # caller who wrapped `docopt()' in `except Exception' does NOT catch a usage # error: the program exits. `DocoptLanguageError' -- what a malformed DOC raises # -- derives from plain `Exception' and IS caught by that handler, which is the # opposite behaviour for the more serious of the two faults. Both bases are # pinned (`exit-base', `langerr-base'), because they are what a caller's error # handling is written against and nothing behavioural would notice a change. # # `-h' and `--version' exit through `extras()', which calls `sys.exit()' with NO # ARGUMENT: the code is `None', not 0. A shell sees status 0; a caller # inspecting `.code' sees `None'. Both are asserted, separately, because they # are different statements about the same call. # # And `--version' IS INERT WITHOUT A `version=' ARGUMENT -- it parses as an # ordinary flag and prints nothing, even though the usage line looks # self-sufficient. That is the single most likely way to misuse this package, # and `version-inert' is the case that states it. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, ONE FILE, python3-core AND NOTHING ELSE. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), so a module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. # # docopt ships no C: setup.py declares `py_modules=['docopt']' and nothing else, # and the module imports `sys' and `re'. `pkg-depends-core' is what would say so # if a release grew a third-party requirement, and `pkg-no-so' catches an # extension at INSTALL rather than at `import'. # # It is a MODULE, NOT A PACKAGE -- ONE FILE lands in site-packages rather than a # directory. `no-package-dir' and `is-module' assert that directly, because a # release that moved to a package would still import and still pass every # behavioural case in this file. # # THERE IS NO SHIPPED SCRIPT. Unlike python3-smartypants, this package declares # no `scripts' and no `console_scripts', so nothing lands in ${bindir} and there # is no shebang to assert. `pkg-no-bin' states that as a fact about the package # rather than leaving it to be inferred from an absence. # # ============================================================================ # THE THREE CASES THAT ARE NOT RUN IN THE PROBE, AND WHY. # ============================================================================ # # `cli-*' run data/docopt-cli-example.py as a REAL PROCESS. What they measure is # what a caller's shell sees -- the text on the right STREAM and the exit STATUS # -- and neither is observable from inside the interpreter that is exiting. The # help goes to STDOUT and exits 0; a rejected argv's usage goes to STDERR # (CPython prints an uncaught SystemExit's string code there) and exits 1. # # `cli-help-whole' is the case the ticket asks for in as many words: the printed # text is captured THROUGH A PIPE and asserted against the usage string that # produced it -- the example's own docstring, extracted from the shipped file by # awk rather than re-typed, so the two cannot drift apart. # # ============================================================================ # NO ptest QUESTION ARISES. # ============================================================================ # # The recipe does not `inherit ptest', so no -ptest package could be produced # whatever DISTRO_FEATURES said. Upstream's test suite lives in the git # repository and is NOT in the sdist this recipe fetches -- the sdist ships # docopt.py, README.rst, setup.py, LICENSE-MIT and an examples/ directory, and # no tests. What those tests cover is reproduced case by case in the probe # instead, and `examples/naval_fate.py' is the source of the CLI example's shape. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the sdist verified against # the recipe's own SRC_URI sha256 (49b3a825...c491) before it was read. The # control interpreter is CPython 3.12.3 and the image ships 3.12, so the two are # the same minor -- which matters here more than usual, because `import-quiet' # below is about a diagnostic 3.12 introduced. The on-target result is not yet # measured: the batch verify is this port's first run on wasm32-hwjs. P=python3-docopt # THE CANARY, which asserts nothing about python3-docopt. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present require awk $P/awk-present SITE=/usr/lib/python3.12/site-packages MOD=$SITE/docopt.py DI=$SITE/docopt-0.6.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-docopt.list PROBE=$BST_DATA/python3-docopt-probe.py EXAMPLE=$BST_DATA/docopt-cli-example.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-docopt.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import docopt' further down is the functional # statement; these are the ones that say WHICH copy answered, so a package that # imported out of some other path on sys.path is distinguishable from one that # imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-file test -s "$MOD" # ONE FILE, NOT A DIRECTORY. See the header: `py_modules=['docopt']' is the # opposite shape from a package, and this is the case that fails a release which # moved to one -- loudly, here, rather than nowhere. check $P/no-package-dir test ! -d "$SITE/docopt" # The precompiled bytecode, which is what `import-quiet' below turns on. check $P/pycache test -s "$SITE/__pycache__/docopt.cpython-312.pyc" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. See the header: on # this image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- at install rather than at `import'. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...AND NOTHING IN ${bindir}. See the header: this package declares no # `scripts' and no `console_scripts', so there is no executable and no shebang # to assert. Stated as a fact about the package rather than left to be inferred # from a case nobody wrote. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The two cases # above name the failures that are expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata. This module carries its own `__version__', so the # dist-info is the SECOND of two independent statements of the version rather # than the only one, and `version-agrees' below is what compares them. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.6.2" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import docopt' has to match, and a wheel that renamed the module would change # it here first. check_out $P/dist-info-top-level "docopt" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points -- so a release that renamed or dropped it # breaks the recipe's checksum, and this case says which of the two happened. check $P/dist-info-license test -s "$DI/LICENSE-MIT" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-docopt is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-docopt$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCY THE RECIPE DECLARES, as dpkg recorded it -- and here it # is the WHOLE of it. docopt imports `sys' and `re' and nothing else, so the # built package depends on python3-core alone; that is why this port waits on no # other package, and this case is what would say so if a release grew a # third-party requirement. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If docopt cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import docopt' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is not a style assertion, # it is a statement about the shipped bytecode. docopt.py contains FOUR regular # expressions written as non-raw string literals -- `'(<\S*?>)'', `'\[default: # (.*)\]'' twice, and `'\n *(<\S+?>|-\S+?)'' -- whose `\S' and `\[' are invalid # escape sequences. CPython 3.12 raises those from DeprecationWarning to # SyntaxWarning, which is shown by DEFAULT, and MEASURED on the control # interpreter (3.12.3) they print four lines naming lines 165, 166, 207 and 456. # # They are not printed here because the package ships a precompiled .pyc: the # warning is raised by the COMPILER, so a module loaded from cached bytecode # never emits it. A red here means the shipped .pyc was REJECTED -- a stale # hash, a python3 minor bump that changed the magic, or a missing __pycache__ -- # and the module is being compiled from source on every import. # # This is the one case in this file that is a statement about the PLATFORM's # packaging rather than about docopt, and it is the reason the control # interpreter being the same minor as the target's (3.12) is worth stating. check_eq $P/import-quiet "" \ "$(python3 -c 'import docopt' 2>&1)" # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the `^' and `=' anchors keep `short_on' # distinguishable from `shortcut_none'. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "84" "$(pv emitted_count)" # The module answered out of the file this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the # behavioural cases below statements about the SAME docopt. check_eq $P/module-answered "$MOD" "$(pv module_file)" # ...and it is a MODULE. See the header. check_eq $P/is-module "yes" "$(pv is_module)" # THE VERSION, FROM BOTH PLACES, AND THEN AGAINST EACH OTHER. The module's own # `__version__' and the installed dist-info read through importlib.metadata are # independent statements; `version-agrees' is the one that catches a module that # landed beside a dist-info describing some other release, which either case # alone would pass. check_eq $P/version-attr "0.6.2" "$(pv version_attr)" check_eq $P/dist-version "0.6.2" "$(pv dist_version)" check_eq $P/version-agrees "$(pv version_attr)" "$(pv dist_version)" # `__all__' IS ONE NAME. docopt publishes the function and nothing else; the # pattern classes are reachable but unpublished, and a release that promoted one # would say so here. check_eq $P/all-names "docopt" "$(pv all_names)" # --------------------------------------------------------- the exception API --- # THE TWO EXCEPTIONS AND THEIR BASES. See the header: this is what a caller's # error handling is written against, and nothing behavioural would notice a # change. `DocoptExit' IS a SystemExit -- so `except Exception' around # `docopt()' does NOT catch a usage error and the program exits instead -- while # `DocoptLanguageError', raised by a malformed DOC, is an ordinary Exception and # IS caught by that same handler. check_eq $P/exit-base "SystemExit" "$(pv exit_base)" check_eq $P/exit-is-systemexit "yes" "$(pv exit_is_systemexit)" check_eq $P/langerr-base "Exception" "$(pv langerr_base)" check_eq $P/langerr-not-exit "no" "$(pv langerr_is_systemexit)" # The pattern classes the parser builds its tree out of. No case reaches them # directly; they are asserted so that a red in the behavioural groups has # somewhere to start -- a missing class here says the module is not the one this # suite was written against. for _n in Pattern ChildPattern ParentPattern Argument Command Option Required \ Optional AnyOptions OneOrMore Either TokenStream Dict; do check_eq $P/class-$(echo "$_n" | tr 'A-Z' 'a-z') "yes" "$(pv has_$_n)" done # ------------------------------------------------------- the ten constructs --- # Each construct the usage grammar defines, both ways. See the header for what # the ABSENT half means in each case -- the values are not one thing, and that # is the part a caller has to know. # 1. A SHORT OPTION. check_eq $P/short-on "-v=True" "$(pv short_on)" check_eq $P/short-off "-v=False" "$(pv short_off)" # 2. A LONG OPTION. check_eq $P/long-on "--verbose=True" "$(pv long_on)" check_eq $P/long-off "--verbose=False" "$(pv long_off)" # 3. AN OPTION TAKING AN ARGUMENT, in both spellings a caller may write, and # absent -- where it is `None' rather than `False', because it has a value slot. # The two spellings are asserted against the same literal rather than against # each other, so a release that broke one of them cannot be masked by the other. check_eq $P/optarg-space "--num='5'" "$(pv optarg_space)" check_eq $P/optarg-equals "--num='5'" "$(pv optarg_equals)" check_eq $P/optarg-absent "--num=None" "$(pv optarg_absent)" # 4. A REPEATED OPTION IS COUNTED, and the count is an INTEGER -- see the header # for why that distinction needed a canonical form to be visible at all. The # stacked spelling is the one a user actually types and reaches a different # branch of `parse_shorts'. check_eq $P/counted-one "-v=1" "$(pv counted_one)" check_eq $P/counted-three "-v=3" "$(pv counted_three)" check_eq $P/counted-stacked "-v=3" "$(pv counted_stacked)" # 5. A POSITIONAL ARGUMENT, required. Missing it is a usage error, which is the # half that says the angle brackets are not decoration. check_eq $P/positional "='a.txt'" "$(pv positional)" check_eq $P/positional-missing ">" "$(pv positional_missing)" # 6. AN OPTIONAL GROUP. Absent, the KEY IS STILL THERE and the value is `None'. check_eq $P/optional-given "='a.txt'" "$(pv optional_given)" check_eq $P/optional-absent "=None" "$(pv optional_absent)" # 7. A REQUIRED GROUP: both members or neither. check_eq $P/required-both "='x',='y'" "$(pv required_both)" check_eq $P/required-one " )>" "$(pv required_one)" # 8. MUTUAL EXCLUSION. Either alone parses; NEITHER is an error and so is BOTH. # The last two are what make the pipe mean "exactly one" rather than "at most # one" or "at least one", and a suite with only the first two could not tell the # three apart. check_eq $P/either-fast "--fast=True,--slow=False" "$(pv either_fast)" check_eq $P/either-slow "--fast=False,--slow=True" "$(pv either_slow)" check_eq $P/either-neither "" \ "$(pv either_neither)" check_eq $P/either-both "" \ "$(pv either_both)" # 9. `...' FOR REPETITION. The value is a LIST, and it is a list even for ONE # argument -- which is the property a caller depends on and the one a naive # implementation gets wrong by returning a bare string for the single case. check_eq $P/repeat-one "=['a']" "$(pv repeat_one)" check_eq $P/repeat-three "=['a', 'b', 'c']" "$(pv repeat_three)" check_eq $P/repeat-none "...>" "$(pv repeat_none)" # 10. THE `[options]' SHORTCUT. Every option the Options section describes and # the usage pattern does NOT name becomes accepted -- 0.6.0's redefinition, from # "any known option" to "any option not in the pattern". `shortcut-none' is the # half that says the shortcut also puts every one of them in the RESULT, with # its absent value, rather than only accepting them. check_eq $P/shortcut-none "--num=None,--quiet=False,-a=False,='80'" \ "$(pv shortcut_none)" check_eq $P/shortcut-some "--num='3',--quiet=False,-a=True,='80'" \ "$(pv shortcut_some)" # ...and the short spelling of a long option resolves to the SAME key: `-n 3' # comes back under `--num', because `Option.name' is the long form when there is # one. A caller indexing by `-n' would find nothing, and this is the case that # says so. check_eq $P/shortcut-all "--num='3',--quiet=True,-a=True,='80'" \ "$(pv shortcut_all)" # A COMMAND is a bare word in the pattern and comes back as a BOOLEAN keyed by # the word itself -- the shape that makes a multi-command program work. This is # upstream's own two-line example. `command-serial' is the more informative of # the pair: the branch that was NOT taken still contributes its elements, so # `' is `None' and `tcp' is `False' rather than absent. check_eq $P/command-tcp \ "--baud='9600',--timeout='30',='127.0.0.1',='80',serial=False,tcp=True" \ "$(pv command_tcp)" check_eq $P/command-serial \ "--baud='115200',--timeout=None,=None,='/dev/ttyS0',serial=True,tcp=False" \ "$(pv command_serial)" # ------------------------------------------------------------- the defaults --- # THE DEFAULTS SECTION, asserted on its own. An option whose description carries # `[default: ...]' comes back with that value when absent from argv, and with # the given value when present. The pair is the assertion: a default that could # not be overridden would pass the first case alone. check_eq $P/default-absent "--name=None,--num='7'" "$(pv default_absent)" check_eq $P/default-given "--name=None,--num='9'" "$(pv default_given)" # AN OPTION WITH NO DEFAULT comes back `None' beside one that has a default, in # the SAME dictionary -- so the marker is read per option rather than applied to # the section. check_eq $P/default-partial "--name='x',--num='7'" "$(pv default_partial)" # THE MARKER IS CASE-INSENSITIVE, and it is read out of the DESCRIPTION -- the # text after the two-space gap -- rather than out of the whole line. check_eq $P/default-uppercase "--num='7'" "$(pv default_uppercase)" # A DEFAULT ON A REPEATABLE OPTION IS SPLIT ON WHITESPACE INTO A LIST. 0.6.0's # documented behaviour, and the one place a default's TYPE depends on the usage # PATTERN rather than on the description line that carries it. check_eq $P/default-repeatable "--path=['./here', './there']" \ "$(pv default_repeatable)" # A DEFAULT IS NOT READ FOR A FLAG THAT TAKES NO ARGUMENT: `Option.parse' only # looks for the marker when `argcount' is 1, so the text is inert and the flag # comes back `False'. Recorded because the line LOOKS like it sets one, and a # reader who wrote it would otherwise have no way to find out it did nothing. check_eq $P/default-on-flag "--flag=False" "$(pv default_on_flag)" # -------------------------------------------------------- the failure paths --- # AN argv THE USAGE DOES NOT ADMIT, in the three shapes it takes: an option the # doc never named, a positional the pattern has no slot for, and an option whose # required argument is missing. All three raise `DocoptExit' -- a SystemExit, so # an uncaught one exits the program. check_eq $P/bad-unknown-option "" \ "$(pv bad_unknown_option)" check_eq $P/bad-extra-arg "" \ "$(pv bad_extra_arg)" # The missing-argument case is the one that carries a MESSAGE of its own ahead # of the usage. The probe re-joins the two lines on `|', which is why this # literal has one. check_eq $P/bad-missing-arg \ "]>" \ "$(pv bad_missing_arg)" # THE EXCEPTION CARRIES THE USAGE, which is what makes the failure useful: what # a user sees is the usage section of the very doc that rejected them. check_eq $P/exit-message "Usage: prog " "$(pv exit_message)" # A MALFORMED DOC is the developer's error, not the user's, and it is the OTHER # exception. No `usage:' at all, and then two of them -- ambiguous rather than # generous. Both are `DocoptLanguageError', which is not a SystemExit. check_eq $P/no-usage "" "$(pv no_usage)" check_eq $P/two-usage "" "$(pv two_usage)" # ------------------------------------------------ -h and --version, in-process --- # WHAT THEY PRINT, captured in-process and asserted against the string that # produced it. `help-is-doc' is the equality that matters: `extras()' prints # `doc.strip("\n")' -- the WHOLE doc, not just its usage section -- and stating # it as an equality against the input rather than as a second literal means a # release that changed the stripping says so here rather than in a literal a # reader cannot check. The probe re-joins on `|'. check_eq $P/help-short-out "$(pv help_is_doc)" "$(pv help_short_out)" check_eq $P/help-long-out "$(pv help_is_doc)" "$(pv help_long_out)" # ...AND THE EXIT CODE IS `None', NOT 0. `extras()' calls `sys.exit()' with no # argument. A shell sees status 0 -- `cli-help-rc' below asserts that half -- but # a caller inspecting `SystemExit.code' sees `None', and the two are different # statements about the same call. check_eq $P/help-short-code "None" "$(pv help_short_code)" check_eq $P/help-long-code "None" "$(pv help_long_code)" # THE HELP EXIT HAPPENS BEFORE THE PATTERN IS MATCHED: `-h' alone prints and # exits even though `' is required and missing, because `extras()' runs # ahead of the match. Without this case a reader would reasonably assume help is # reached only by an otherwise-valid command line. check_eq $P/help-before-match "yes" "$(pv help_before_match)" # `--version' PRINTS THE OBJECT PASSED AS `version=', and nothing else. check_eq $P/version-out "prog 1.2.3" "$(pv version_out)" check_eq $P/version-code "None" "$(pv version_code)" # ...AND IS INERT WITHOUT IT. See the header: this is the package's most likely # misuse. With no `version=' argument `--version' is an ordinary flag -- it # parses, comes back `True', prints nothing and does not exit. check_eq $P/version-inert "--help=False,--version=True,='a.txt'" \ "$(pv version_inert)" # `help=False' does the same to `-h', which says the automatic help is a # behaviour of `docopt()' rather than of the `-h' spelling. check_eq $P/help-disabled "--help=True,--version=False,='a.txt'" \ "$(pv help_disabled)" # ------------------------------------------------------------- the corners --- # `options_first', 0.6.0's addition: with it set, the FIRST positional ends the # option section and everything after it is a positional -- which is how a # program that forwards a sub-command's own options is written. The default is # the interesting comparison: the SAME argv gives `-v=True' with an empty # `' one way and `-v=False' with `-v' inside `' the other. check_eq $P/options-first-off "-v=True,=[],='run'" \ "$(pv options_first_off)" check_eq $P/options-first-on "-v=False,=['-v'],='run'" \ "$(pv options_first_on)" # THE `--' SEPARATOR, which ends option parsing whatever `options_first' says. # Note the key: `--' is itself in the returned dictionary, because the usage # pattern names it -- so a caller who wrote `[--]' gets a key back for it. check_eq $P/double-dash "--=True,-v=True,=['-x', 'y']" "$(pv double_dash)" # THE RETURN TYPE is `docopt.Dict', a `dict' SUBCLASS -- so a caller may treat # it as a dictionary. The subclass exists only for its sorted multi-line # `__repr__', which is why the probe never prints one and why every result in # this file goes through a canonical one-line form instead. check_eq $P/result-type "Dict" "$(pv result_type)" check_eq $P/result-is-dict "yes" "$(pv result_is_dict)" check_eq $P/result-keys "-v" "$(pv result_keys)" # AN EMPTY PATTERN AND AN EMPTY argv is a legal parse returning an EMPTY # dictionary. A function that raised on it would fail no case above. check_eq $P/empty-pattern "<>" "$(pv empty_pattern)" # `usage:' IS MATCHED CASE-INSENSITIVELY. Both spellings, because the regex is # character-by-character rather than a flag, and a reader meeting `UsAgE:' in # `printable_usage()' would reasonably wonder whether it is deliberate. check_eq $P/usage-lowercase "='a'" "$(pv usage_lowercase)" check_eq $P/usage-mixedcase "='a'" "$(pv usage_mixedcase)" # AN UPPERCASE WORD IS AN ARGUMENT, NOT A COMMAND -- `parse_atom' accepts either # `' or an all-caps token, and the key keeps the caps. Without this case # a reader would have to work out from `token.isupper()' why `prog FILE' does # not produce a boolean the way `prog tcp' does. check_eq $P/uppercase-arg "FILE='a.txt'" "$(pv uppercase_arg)" # ------------------------------------------------------ the exiting paths, run --- # THE THREE CASES THAT ARE NOT IN THE PROBE. See the header: what these measure # is what a caller's SHELL sees -- the text on the right STREAM and the exit # STATUS -- and neither is observable from inside the interpreter that is # exiting. if [ ! -f "$EXAMPLE" ]; then bst_notok $P/example-present "$EXAMPLE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/example-present # The example's own docstring, extracted from the shipped file rather than # re-typed, so the expectation and the program cannot drift apart. It is copied # to tmpfs first for the same dup2(2) reason as the dpkg list above. cp "$EXAMPLE" "$W/ex.py" 2>/dev/null || : > "$W/ex.py" awk '/^"""/{ if(!p){p=1; s=substr($0,4); if(s!="") print s; next} else exit } p' \ "$W/ex.py" > "$W/doc" 2>/dev/null check $P/example-doc-extracted test -s "$W/doc" # ...and its USAGE SECTION alone, which is what a rejected argv prints: the # section ends at the first BLANK LINE, which is `printable_usage()' and is the # reason the docstring has one there. awk 'NF==0{exit} {print}' "$W/doc" > "$W/usage" 2>/dev/null check $P/example-usage-extracted test -s "$W/usage" # THE HELP, THROUGH A PIPE, ASSERTED AGAINST THE STRING THAT PRODUCED IT. This # is the ticket's own requirement in as many words. `cat |' is not decoration: # the pipe forks, so this never performs a top-level stdin redirect on the # virtiofs root (brintos/linux#73). check_eq $P/cli-help-whole "$(cat "$W/doc")" \ "$(python3 "$W/ex.py" -h 2>/dev/null)" # ...ON STDOUT, and exiting 0. `extras()' calls `sys.exit()' with no argument; # `help-short-code' above asserts that the code object is `None', and this is # the same call seen from the shell, where `None' presents as 0. check_rc $P/cli-help-rc 0 python3 "$W/ex.py" -h # `--version' prints the object passed as `version=' and nothing else, also on # stdout, also exiting 0. check_eq $P/cli-version "docopt-cli-example 1.0" \ "$(python3 "$W/ex.py" --version 2>/dev/null)" # A REJECTED argv GOES TO STDERR AND EXITS 1, which is the opposite of the two # above on both counts. CPython prints an uncaught SystemExit's string code to # stderr, and the string is the usage section -- so this asserts the STREAM # (stdout is empty), the TEXT (against the extracted usage) and the STATUS, # which together are the whole of what a shell caller can observe. check_eq $P/cli-bad-stdout "" "$(python3 "$W/ex.py" bogus 2>/dev/null)" check_eq $P/cli-bad-stderr "$(cat "$W/usage")" \ "$(python3 "$W/ex.py" bogus 2>&1 >/dev/null)" check_rc $P/cli-bad-rc 1 python3 "$W/ex.py" bogus # ...AND A VALID argv PARSES, in the same process shape, so the three failures # above are statements about the exiting paths rather than about the example # being unable to run at all. check_eq $P/cli-parse \ "--help=False,--speed='10',--version=False,='a',='1',='2',mine=False,move=True,remove=False,set=False,ship=True" \ "$(python3 "$W/ex.py" ship a move 1 2 2>/dev/null)" rm -rf "$W"