. "$BST_LIB" # BST-TIMEOUT: 900 # # ESTIMATED, NOT MEASURED. This suite was authored in the recipe phase of a # batched Epic pass (BRINTOS-2295, batch-2271), which forbids building the image # and so could not run it. The bound is sized from the neighbouring python # ports rather than from a run of this file: ONE interpreter start at ~18 s # (99-pyxdg.test's header, batch-1649), one `import setuptools`, two # `Distribution` constructions, and shell builtins over a dpkg file list for # everything else. 900 is what 99-python3-setuptools.test gives a strictly # larger measurement -- an interpreter start, a setuptools import AND an sdist # build -- so it is a backstop against a slow start rather than a prediction of # the runtime. 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-versiontools 1.9.1 (meta-openembedded meta-python/recipes-devtools/ # python/python3-versiontools_1.9.1.bb -- `inherit pypi setuptools3`, PyPI # sdist) -- THE SMART REPLACEMENT FOR THE PLAIN TUPLE A PACKAGE PUTS IN # `__version__`, on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS, IN ONE PARAGRAPH. # ============================================================================ # # `versiontools.Version` is a FIVE-FIELD TUPLE SUBCLASS -- (major, minor, micro, # releaselevel, serial), the same shape as `sys.version_info` -- that ALSO # renders itself as a PEP 440-shaped string, and that hooks into `setup.py` so a # package's version is DERIVED from that tuple rather than duplicated beside it # as a literal. Four source files, all pure Python, plus three VCS plugins that # are never reached on this image. # # ============================================================================ # WHY THE TWO HALVES ARE ASSERTED SEPARATELY. # ============================================================================ # # `__str__` is overridden and `__repr__` IS NOT. So a Version compares, sorts, # unpacks and hashes EXACTLY as the equivalent plain tuple does -- that is the # whole point of subclassing `tuple` -- while printing as something else # entirely. The two halves fail independently: a broken `__new__` would leave # every string case green, and a broken formatter would leave every ordering # case green. Hence `tuple-*`/`order-*` on one side and `s-*`/`token-*` on the # other, over the same literals. # # THE ORDER IS PLAIN TUPLE ORDER, AND THAT IS A FACT ABOUT STRINGS. Element 3 is # compared as text, which happens to give the semantically right answer for the # four levels this port's ticket names -- "alpha" < "beta" < "candidate" < # "final" lexicographically -- with the serial breaking ties inside a level. # `order-dev-after-cand` pins the ONE PLACE where the lexical order and the # semantic order disagree: "dev" is the earliest level the class permits and it # sorts BETWEEN "candidate" and "final", because c < d < f. That is a property # of the package, not a defect of the port, and it is asserted so a consumer is # not surprised by it later. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY SAYING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module # carrying a compiled extension could therefore not be imported here -- not # "would be slower", could not be imported. versiontools is pure Python, which # is the entire reason it ports. # # So `module-no-so` and `pkg-no-so` are not boilerplate. They are a REGRESSION # TRIPWIRE on a version bump: versiontools grows no extension today, and the day # a release adds one these go red at INSTALL time rather than at # `import versiontools` on somebody else's ticket three rounds later. The # assertion is made from BOTH sides because the two fail differently -- a find # catches a file the package does not declare, the dpkg list catches a declared # file that is not Python. # # ============================================================================ # THE ONE RUNTIME DEPENDENCY, AND WHY THIS SUITE LEANS ON IT. # ============================================================================ # # `RDEPENDS:${PN} += "python3-setuptools"`, and setuptools is on this image # (BRINTOS-2237). That is what makes the `dist-*` and `pkginfo-*` cases below # possible: they drive a REAL `setuptools.dist.Distribution`, so the end-to-end # statement -- setuptools reads the `distutils.setup_keywords` entry point out # of the metadata THIS IMAGE installed, calls versiontools, and the version that # lands in PKG-INFO is the one the fixture's tuple describes -- is measured # rather than assumed. It is also the reason `import versiontools` works at all # on python3.12, whose stdlib has no `distutils`: the last line of # `__init__.py` imports `setuptools_hooks`, which imports `distutils.errors`, # which resolves only through setuptools' `_distutils_hack` # (99-python3-setuptools.test's `distutils-hack` and `distutils-pth` assert that # shim is installed). # # NOTE THE NORMALISATION, which is the subtlest thing this suite pins. # versiontools renders a candidate release as `c2`; setuptools then REWRITES it # to PEP 440's `rc2` on its way into the metadata. So `hook-magic` (3.1.4c2) and # `dist-version` (3.1.4rc2) DIFFER, by design and at setuptools' hand. Both are # asserted, at their own values -- reading either as the other's is the mistake # this pair exists to prevent. # # ============================================================================ # EVERY VALUE ASSERTED BELOW WAS MEASURED, NOT PREDICTED -- WITH ONE EXCEPTION. # ============================================================================ # # The control is an x86-64 run of THIS EXACT PROBE against versiontools 1.9.1 # from the same PyPI sdist this recipe builds, under CPython 3.12 and setuptools # 69.1.1 -- the same setuptools this image installs (recorded 2026-09-07; kept # in the ticket's design doc and test-plan section BRINTOS-2295). What makes the # control transferable is that ALL of it is pure-Python and content-determined: # integer comparison, string formatting over `%s`/`%d`, `sorted()` over tuples # and a text search of a source line. Nothing here depends on pointer width, # byte order or the platform string, and nothing arch-dependent is asserted. # # THE PACKAGING SIDE IS NOT TAKEN FROM THE CONTROL AT ALL. `pkg-version`, # `pkg-depends`, the file-list cases and the entry-point text are statements # about the .deb THIS build produced, read off # python3-versiontools_1.9.1-r0_wasm32.deb -- built at recipe scope on dmi2, # 2026-09-07, 1641 tasks attempted and all succeeded, zero ERROR: lines. Its # `entry_points.txt` and its six source files are BYTE-IDENTICAL to the # control's install, which is what makes the control's behavioural values # transferable rather than merely plausible. # # CPYTHON'S OWN WORDING IS NEVER ASSERTED. Four failures carry a message # CPython writes -- an ImportError's "No module named", an AttributeError's # wording, `int()`'s "invalid literal", `__new__`'s arity -- and those keys # report the exception TYPE only, with a separate `-msg` case asserting that # versiontools' own prefix is still there. A python3 bump rewords the # interpreter's half regularly and must not turn a working port red. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE VCS INTEGRATION. `Version.vcs` queries bzr, git and hg through the # `versiontools.vcs_integration` entry points, and none of those three tools # is on this image. Each plugin's `from_source_tree` swallows its own # ImportError and logs at DEBUG, so the query returns None rather than # failing -- which is why `s-dev` asserts `1.3.dev` with NO revision suffix. # That is the arm this image will always take, and building a repository to # reach the other one would be measuring bzrlib, not versiontools. The entry # points themselves ARE asserted (`ep-vcs-names`): the wiring is the part # that packaging can silently drop. # - A SUBPROCESS `setup.py` RUN. It would cost a second interpreter start -- # tens of seconds on this arm -- to measure setuptools' command machinery # rather than versiontools. The hook is driven directly AND through a real # `Distribution`, and the metadata it produces is written to disk and read # back, which is the half of that run versiontools owns. # - `versiontools.tests`. The package ships its own unittest module; it exercises # the same surface this suite asserts, from inside the package, and running # it would report a pass/fail count rather than the per-behaviour cases the # project's suites are read for. # - python3-versiontools-dev / -dbg. Produced by the build; the image names # neither. -dev is EMPTY -- measured: its data archive carries `./` and # nothing else -- so there is no -dev content a case here could assert. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. Every fixture is written by the probe # itself, so the suite reaches no device, no peer and no terminal. P=python3-versiontools # THE CANARY, which asserts nothing about python3-versiontools. # # 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` guard deliberately, # because a `require` that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages VT=$SITE/versiontools DI=$SITE/versiontools-1.9.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-versiontools.list D=$BST_DATA PROBE=$D/python3-versiontools-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-versiontools.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import versiontools'` further down # is the functional statement; these are the ones that say WHICH copy answered, # so a module that imported out of some other path is distinguishable from one # that imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$VT" # THE FOUR SOURCE FILES, NAMED SEPARATELY BECAUSE THEY FAIL DIFFERENTLY. A # missing `__init__.py` breaks the import outright; a missing # `setuptools_hooks.py` breaks it too, because `__init__.py`'s LAST LINE imports # it -- which is a fact worth having a case for, since a reader would otherwise # expect the hooks to be optional. `versiontools_support.py` is the standalone # copy a consuming project vendors, and its absence leaves everything else here # green. check $P/module-init test -f "$VT/__init__.py" check $P/module-hooks test -f "$VT/setuptools_hooks.py" check $P/module-support test -f "$VT/versiontools_support.py" # The package's own unittest module, shipped by this build. Not RUN here -- see # the header -- but named, because its absence would mean the wheel dropped a # top-level source file and no other case would say which. check $P/module-tests test -f "$VT/tests.py" # The three VCS plugins. Never reached on this image (see the header), and # asserted present because the entry points below advertise them: an entry point # whose target file did not ship raises at `load()` rather than being ignored. check $P/module-bzr test -f "$VT/bzr_support.py" check $P/module-git test -f "$VT/git_support.py" check $P/module-hg test -f "$VT/hg_support.py" # THE DIST-INFO DIRECTORY CARRIES THE VERSION IN ITS NAME. This is a second, # independent statement of the version -- from the installed layout rather than # from the metadata the interpreter reads -- and the two disagreeing would mean # a mixed install that no single case could otherwise see. check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-metadata test -f "$DI/METADATA" # THE ENTRY-POINT METADATA. This is the whole of versiontools' wiring into # setuptools, and it is the one file a wheel build can silently drop while # producing a package that imports perfectly and does nothing. Asserted here as # a file and as text, and LOADED through the probe further down. check $P/dist-info-entry-points test -f "$DI/entry_points.txt" check_out $P/entry-points-keyword \ "version = versiontools.setuptools_hooks:version" cat "$DI/entry_points.txt" check_out $P/entry-points-group "[distutils.setup_keywords]" \ cat "$DI/entry_points.txt" check_out $P/entry-points-vcs-group "[versiontools.vcs_integration]" \ cat "$DI/entry_points.txt" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. 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 -- loudly, here, instead of at `import` later. check_eq $P/module-no-so "" \ "$(find "$VT" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # # 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)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # THE `-f` TEST IS LOAD-BEARING. A dpkg .list names DIRECTORIES too, and without # a trailing slash, so a name-only filter reports every directory in the package # as an unexpected shape and this case would be red on a CORRECT package # (measured on python3-pathspec, BRINTOS-1834). _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -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 ------ # 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-versiontools 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. The # anchor is exact: `python3-versiontools` is a PREFIX of # `python3-versiontools-dev` and `-dbg`, and an unanchored match would report # one stanza as another's. sed -n '/^Package: python3-versiontools$/,/^$/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 "1.9.1-r0" cat "$W/st" # THE DECLARED RUNTIME DEPENDENCY. `RDEPENDS:${PN} += "python3-setuptools"` is # the recipe's one addition, and it is not decorative: without setuptools on the # image there is no `distutils` shim, and `import versiontools` fails at the # last line of `__init__.py`. A version bump that dropped the declaration would # leave this image working by accident and every smaller image broken. check_out $P/pkg-depends-setuptools "python3-setuptools" cat "$W/st" # ...and the WHOLE declaration, which is two entries and no more. Measured off # the built .deb: `Depends: python3-core, python3-setuptools`. The substring # case above survives a reordering; this one is what catches a dependency the # package GREW, which on this image is the difference between a port that ships # and one that waits on another round. check_out $P/pkg-depends "Depends: python3-core, python3-setuptools" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If versiontools 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 versiontools' # ...and the two names the rest of this file is about, imported by name. A # `Version` that did not export would fail here rather than in twenty places. check $P/import-version python3 -c 'from versiontools import Version, format_version' # THE HOOK IMPORT IS THE distutils SHIM, RE-ASSERTED FROM THIS PACKAGE'S SIDE. # `setuptools_hooks` does `from distutils.errors import DistutilsSetupError`, # and python3.12's stdlib has no distutils -- so this case going red would mean # setuptools' `_distutils_hack` did not take, not that versiontools is broken. check $P/import-hooks python3 -c 'from versiontools.setuptools_hooks import version' # ------------------------------------------------------------- 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" 600 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 anchors keep `err_expr_import` and # `err_expr_import_msg` distinguishable. 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 package, 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 # A traceback the probe caught itself, surfaced as its own case rather than as # ninety separate got:'' reds. check_eq $P/probe-no-fatal "" "$(pv fatal)" # ------------------------------------------------------------ identity ------- # THE VERSION THE RECIPE PINS, read through importlib.metadata rather than from # dpkg. A mismatch here means the recipe and what actually landed have gone out # of step, and every expectation in this file is then describing a different # versiontools. check_eq $P/version "1.9.1" "$(pv version)" check_eq $P/version-readable "" "$(pv version_error)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME versiontools. check_eq $P/module-file "$VT/__init__.py" "$(pv module_file)" check_eq $P/hooks-file "$VT/setuptools_hooks.py" "$(pv hooks_file)" # ...and it answered out of a .py, not an extension. The third statement of the # .so negative, this one from inside the interpreter. check_eq $P/module-is-py "yes" "$(pv module_is_py)" # THE PACKAGE'S OWN `__version__` IS A PLAIN TUPLE, NOT A Version. versiontools # eats its own cooking only at `setup()` time; the attribute itself is the # ordinary five-tuple the class exists to replace, and a reader who assumed # otherwise would expect `str(versiontools.__version__)` to be "1.9.1". check_eq $P/self-version-tuple "(1, 9, 1, 'final', 0)" "$(pv self_version_tuple)" check_eq $P/self-version-plain "yes" "$(pv self_version_is_plain_tuple)" # The two ends of the package meeting: its own tuple, put through its own # formatter, is the version the recipe shipped and `version` above read. check_eq $P/self-version-str "1.9.1" "$(pv self_version_str)" # ------------------------------------------------------------- the API ------- # The public surface, named one attribute at a time: `format_version` missing # and `from_expression` missing are different breakages and a single "the API is # there" case would report them identically. check_eq $P/api-version "yes" "$(pv api_version)" check_eq $P/api-format-version "yes" "$(pv api_format_version)" check_eq $P/api-handle-version "yes" "$(pv api_handle_version)" check_eq $P/api-from-tuple "yes" "$(pv api_from_tuple)" check_eq $P/api-from-tuple-and-hint "yes" "$(pv api_from_tuple_and_hint)" check_eq $P/api-from-expression "yes" "$(pv api_from_expression)" check_eq $P/api-hook-version "yes" "$(pv api_hook_version)" # `versiontools.handle_version` IS the setuptools hook, re-exported by the last # line of `__init__.py`. A second copy on sys.path would satisfy every other # case here and fail only this one. check_eq $P/api-handle-is-hook "yes" "$(pv api_handle_is_hook)" # ------------------------------------------------------- the entry points ---- # versiontools does nothing by being importable. It works because setuptools # finds `version` in the `distutils.setup_keywords` group and calls what it # points at. `dist-info-entry-points` above reads the file; these read it the # way a consumer does. check_eq $P/ep-kw-count "1" "$(pv ep_kw_count)" check_eq $P/ep-kw-value "versiontools.setuptools_hooks:version" "$(pv ep_kw_value)" check_eq $P/ep-kw-loads "yes" "$(pv ep_kw_loads)" # ...and it resolves to the same object `import versiontools.setuptools_hooks` # gives. check_eq $P/ep-kw-is-hook "yes" "$(pv ep_kw_is_hook)" # THE THREE VCS PLUGINS ARE ADVERTISED even though none of the three tools is on # this image. The wiring is what packaging drops silently; the query itself is # covered by `vcs-in-function` below. check_eq $P/ep-vcs-names "bzr,git,hg" "$(pv ep_vcs_names)" check_eq $P/ep-no-error "" "$(pv ep_error)" # ======================================================== THE TUPLE HALF ====== # A Version IS a tuple, and every one of these would still hold if `__str__` had # been deleted. See the header for why that separation is the point. check_eq $P/tuple-isinstance "yes" "$(pv tuple_isinstance)" check_eq $P/tuple-len "5" "$(pv tuple_len)" # THE DEFAULTS, asserted through the equality: `Version(1, 2, 3)` IS # `(1, 2, 3, "final", 0)`, so micro defaults to the third positional and the # level to "final". check_eq $P/tuple-eq-plain "yes" "$(pv tuple_eq_plain)" check_eq $P/tuple-unpack "1,2,3,final,0" "$(pv tuple_unpack)" # A Version hashes as its tuple, so it keys a dict a plain tuple also keys. # `__hash__` is inherited precisely because `__eq__` is not overridden. check_eq $P/tuple-hash-eq "yes" "$(pv tuple_hash_eq)" # `__repr__` IS NOT OVERRIDDEN. Pinned at the plain tuple repr because a future # release adding one would change what every traceback and every `%r` in a # consumer prints -- and no other case here would notice. check_eq $P/tuple-repr "(1, 2, 3, 'final', 0)" "$(pv tuple_repr)" check_eq $P/tuple-repr-not-str "yes" "$(pv tuple_repr_is_not_str)" # The five named fields, each an `operator.itemgetter` property over the tuple. # Named separately so a property wired to the wrong index is identifiable rather # than merely "something is off". check_eq $P/field-major "1" "$(pv f_major)" check_eq $P/field-minor "2" "$(pv f_minor)" check_eq $P/field-micro "3" "$(pv f_micro)" check_eq $P/field-releaselevel "final" "$(pv f_releaselevel)" check_eq $P/field-serial "0" "$(pv f_serial)" # STRINGS ARE COERCED TO int, SILENTLY -- the documented behaviour, and the # reason `Version("1", "2")` is not a type error. The types case is the one that # says the coercion happened rather than the strings merely printing the same. check_eq $P/coerce-str "4,5,6,final,0" "$(pv coerce_str)" check_eq $P/coerce-str-types "int,int,int,str,int" "$(pv coerce_str_types)" # ------------------------------------------------------------ the ordering -- # The whole list, sorted from a shuffled input. This is the case the ticket # names: an ordering over literal versions covering each release level in the # order the package defines, with the serial breaking ties within a level. check_eq $P/order-sorted "1.2.9|1.3a1|1.3a2|1.3b2|1.3c1|1.3|1.3.1" "$(pv order_sorted)" check_eq $P/order-is-declared "yes" "$(pv order_is_declared)" # ...and one comparison at a time, so a red says WHICH neighbouring pair # inverted rather than only that the list came out wrong. check_eq $P/order-alpha-lt-beta "yes" "$(pv order_alpha_lt_beta)" check_eq $P/order-beta-lt-cand "yes" "$(pv order_beta_lt_cand)" check_eq $P/order-cand-lt-final "yes" "$(pv order_cand_lt_final)" check_eq $P/order-serial-ties "yes" "$(pv order_serial_ties)" # THE ONE PLACE THE LEXICAL AND SEMANTIC ORDERS DISAGREE -- see the header. # "dev" is the earliest level the class permits and it sorts between "candidate" # and "final". Asserted, not worked around: it is what the package does. check_eq $P/order-dev-after-cand "yes" "$(pv order_dev_after_cand)" check_eq $P/order-dev-before-final "yes" "$(pv order_dev_before_final)" # ======================================================= THE STRING HALF ====== # Every row of the table in `Version.__str__`'s own docstring, against a # literal, so a formatting change cannot pass unnoticed. check_eq $P/str-final-two "1.2" "$(pv s_final_two)" check_eq $P/str-final-three "1.2.3" "$(pv s_final_three)" check_eq $P/str-alpha "1.3a1" "$(pv s_alpha)" check_eq $P/str-beta "1.3b1" "$(pv s_beta)" check_eq $P/str-candidate "1.3c1" "$(pv s_candidate)" # NO VCS ON THIS IMAGE, so no revision suffix -- see the header. A `1.3.dev54` # here would mean something on this image answered as a version control system, # which would be news. check_eq $P/str-dev "1.3.dev" "$(pv s_dev)" # A ZERO MICRO IS OMITTED AND A NON-ZERO ONE IS NOT. This pair is the whole of # that branch; asserting one of them alone would leave a formatter that always # dropped -- or always kept -- the micro entirely green. check_eq $P/str-zero-micro "2.0" "$(pv s_zero_micro)" check_eq $P/str-nonzero-micro "2.0.1" "$(pv s_nonzero_micro)" # A serial above 1 renders as the number, not as a repeated token. check_eq $P/str-serial-10 "1.3b10" "$(pv s_serial_10)" # The same object's repr, beside its str, on a pre-release -- where the two are # least alike. check_eq $P/str-repr-differs "(1, 3, 0, 'candidate', 1)" "$(pv s_repr)" # THE TOKEN TABLE, READ DIRECTLY. `str-alpha`/`str-beta`/`str-candidate` going # wrong could mean the table changed or the formatter did; this case separates # the two. "dev" and "final" are deliberately ABSENT from it -- "final" renders # as nothing at all and "dev" takes its own branch -- so a table that grew an # entry for either would be a formatting change this case catches first. check_eq $P/token-map "alpha=a,beta=b,candidate=c" "$(pv token_map)" # `format_version` is the same formatter reached without naming the class, and # it takes either a Version or a plain five-tuple. Both arms, one case each. check_eq $P/format-version-tuple "1.4b2" "$(pv fv_tuple)" check_eq $P/format-version-version "1.4b2" "$(pv fv_version)" # ---------------------------------------------------------- the round trips -- check_eq $P/roundtrip-from-tuple "1,3,0,candidate,7" "$(pv rt_from_tuple)" check_eq $P/roundtrip-from-tuple-str "1.3c7" "$(pv rt_from_tuple_str)" check_eq $P/roundtrip-from-tuple-eq "yes" "$(pv rt_from_tuple_eq)" # A Version fed back through `from_tuple` is an equal Version -- the identity a # consumer relies on when it round-trips a version through a config file. check_eq $P/roundtrip-idempotent "yes" "$(pv rt_idempotent)" # THE `__version__`-STRING PARSER. `from_expression` imports the module the # expression names and reads the version out of it -- `__version__` by default, # or whatever follows a colon. All three spellings, because the empty-identifier # arm is a separate branch that a reader would not predict from the other two. check_eq $P/expr-default "3,1,4,candidate,2" "$(pv expr_default)" check_eq $P/expr-default-str "3.1.4c2" "$(pv expr_default_str)" check_eq $P/expr-named "0,9,12,final,0" "$(pv expr_named)" check_eq $P/expr-named-str "0.9.12" "$(pv expr_named_str)" check_eq $P/expr-empty-identifier "3.1.4c2" "$(pv expr_empty_identifier)" # ------------------------------------------------------- the source tree ----- # THE HEURISTIC IS LEXICAL, AND THAT IS THE POINT. `Version.__new__` walks the # caller frames for a `` frame whose CURRENT SOURCE LINE contains the # TEXT `__version__` -- not a check that anything is being assigned. The probe # constructs one Version at module level on a line carrying that text and one # inside a function, and the two answers differ: a project that spells its # version variable anything else silently gets no source tree, and therefore # never gets a VCS suffix however many repositories it has. check_eq $P/src-tree-module "$D" "$(pv src_tree_module)" check_eq $P/src-tree-is-probe-dir "yes" "$(pv src_tree_module_is_probe_dir)" check_eq $P/src-tree-function "None" "$(pv src_tree_function)" # ...and with no source tree there is no VCS to query -- the arm `str-dev` took. check_eq $P/vcs-in-function "None" "$(pv vcs_in_function)" # `from_tuple_and_hint` supplies what the heuristic could not find: called from # inside a function it yields None, and the hint module's directory answers # instead. check_eq $P/hint-str "3.1.4c2" "$(pv hint_str)" check_eq $P/hint-source-tree "yes" "$(pv hint_source_tree_is_work)" # ------------------------------------------------------- the error paths ----- # Each of these raises rather than producing a version string nobody can # compare. The messages asserted in full are versiontools' own text; where # CPython writes the message, only the TYPE is asserted -- see the header. check_eq $P/err-releaselevel "ValueError: releaselevel 'nope' is not permitted" \ "$(pv err_releaselevel)" check_eq $P/err-negative-major "ValueError: Version components cannot be negative" \ "$(pv err_negative_major)" check_eq $P/err-negative-serial "ValueError: Version components cannot be negative" \ "$(pv err_negative_serial)" # A PRE-RELEASE WITH SERIAL 0 IS REFUSED: `1.3b0` would sort before every # `1.3bN` and read as "no serial at all". Both levels, because the message names # the level and a wrong one would be invisible with only one case. check_eq $P/err-serial-zero-alpha \ "ValueError: serial must be greater than zero for alpha releases" \ "$(pv err_serial_zero_alpha)" check_eq $P/err-serial-zero-beta \ "ValueError: serial must be greater than zero for beta releases" \ "$(pv err_serial_zero_beta)" # ...and `final` with serial 0 is the ORDINARY case, not an error. Without this # the two above would also pass on a `__new__` that rejected every zero serial. check_eq $P/ok-serial-zero-final "no-raise" "$(pv ok_serial_zero_final)" check_eq $P/err-not-an-int "ValueError" "$(pv err_not_an_int)" # THE WRONG-LENGTH TUPLE, AND WHERE THE GUARD ACTUALLY LIVES. It is in # `format_version`, which requires exactly five items -- NOT in `from_tuple`, # which is `cls(*t)` and therefore takes a THREE-element tuple happily by # falling back on the defaults. Both arms are pinned so a consumer knows which # call to make: `ft-short-no-raise` and `ft-short-value` are what say the # permissive arm is the package's behaviour and not this suite's oversight. check_eq $P/err-fv-short "ValueError: version must be a tuple of five items" \ "$(pv err_fv_short)" check_eq $P/err-fv-long "ValueError: version must be a tuple of five items" \ "$(pv err_fv_long)" check_eq $P/err-fv-not-tuple "ValueError: version must be a tuple of five items" \ "$(pv err_fv_not_tuple)" check_eq $P/err-ft-long "TypeError" "$(pv err_ft_long)" check_eq $P/ft-short-no-raise "no-raise" "$(pv ft_short_no_raise)" check_eq $P/ft-short-value "1.2.3" "$(pv ft_short_value)" check_eq $P/err-expr-import "ValueError" "$(pv err_expr_import)" check_eq $P/err-expr-import-msg "yes" "$(pv err_expr_import_msg)" check_eq $P/err-expr-attr "ValueError" "$(pv err_expr_attr)" check_eq $P/err-expr-attr-msg "yes" "$(pv err_expr_attr_msg)" # =================================================== THE setup.py INTEGRATION = # ASSERTED ON TARGET RATHER THAN ASSUMED -- this is the behaviour the package # exists for, and the one a pure-import suite would leave entirely unmeasured. # # The hook is driven twice: directly, with a duck-typed distribution, which says # the hook itself works; and through a real `setuptools.dist.Distribution`, # which says the entry-point wiring THIS IMAGE installed actually reaches it. check_eq $P/hook-magic "3.1.4c2" "$(pv hook_magic)" # A PLAIN VERSION STRING IS LEFT ALONE. The hook is offered every `setup()` in # the process, so a version assigned here would rewrite the version of a package # that never opted in. `unset` is the fixture's sentinel and seeing it come back # unchanged IS the assertion. check_eq $P/hook-plain "unset" "$(pv hook_plain)" check_eq $P/hook-none "unset" "$(pv hook_none)" # THE `value == 0` ARM, which the hook's own comment calls odd: setuptools # sometimes hands the hook a 0 instead of the string, and the real value is then # read back off `dist.metadata.version`. Without this arm every such call would # silently leave the magic tag in the metadata. check_eq $P/hook-zero "1.0" "$(pv hook_zero)" # A bad expression is re-raised as the error setuptools understands, not as the # ValueError `from_expression` raised -- and versiontools' own text survives the # translation with the magic tag peeled off. check_eq $P/hook-bad "DistutilsSetupError" "$(pv hook_bad)" check_eq $P/hook-bad-msg "yes" "$(pv hook_bad_msg)" # THE END-TO-END STATEMENT. setuptools reads the `distutils.setup_keywords` # entry point out of the metadata this image installed, calls versiontools, and # the distribution's version is the one the fixture's tuple describes. # # `dist-version` IS NOT `hook-magic`. versiontools renders the candidate as # `c2`; setuptools rewrites it to PEP 440's `rc2` on the way into the metadata. # Both are asserted at their own values -- see the header. check_eq $P/dist-version "3.1.4rc2" "$(pv dist_version)" check_eq $P/dist-version-final "1.0" "$(pv dist_version_final)" check_eq $P/dist-no-error "" "$(pv dist_error)" # ...AND THE METADATA IT PRODUCES, ON DISK. `dist.metadata.version` is an # attribute; PKG-INFO is the file a consumer of the sdist actually reads, and # the ticket closes on the version reaching it. check_eq $P/pkginfo-name "vtfixture" "$(pv pkginfo_name)" check_eq $P/pkginfo-version "3.1.4rc2" "$(pv pkginfo_version)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"