. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # third start left behind. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 157 measurements. # # # # The work inside each start is a handful of `re.match' # # calls over strings under 40 bytes and the tuple compares # # they feed -- microseconds beside an interpreter start on # # this arm, which is tens of seconds before the first line # # of the program runs. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-isodate sets for its three, and roughly six # # times the ~60s a python3 start has measured at here. # # Nothing in this suite is quadratic in anything, so the # # start is the only thing the number has to cover. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-semantic-version 2.10.0 (poky/meta # recipes-devtools/python/python3-semantic-version_2.10.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHY THIS IMAGE CARRIES TWO OF THEM. # ============================================================================ # # It implements the SemVer scheme -- the same scheme python3-semver already # ported into this image implements. The rules are a published specification # and neither package is free to differ on them, so what separates the two is # SHAPE, not behaviour: # # python3-semver functions over strings and a `VersionInfo' tuple. # python3-semantic-version a `Version' OBJECT with rich comparison, plus a # RANGE LANGUAGE -- `SimpleSpec' and `NpmSpec' -- # for saying which versions a constraint admits. # # The range language is this package's own and has no counterpart in the other, # which is why the bulk of this suite is about it. The parse is asserted once # per field and then left alone. # # There is no configure step, no compiled extension and no C anywhere in the # package: three `.py' files and a `dist-info'. So there is nothing on this arm # for an override or a site answer to reach, and the recipe is taken from # oe-core exactly as written. # # ============================================================================ # THE THREE FINDINGS THAT ARE THE OPPOSITE OF THE OBVIOUS READING. # ============================================================================ # # Each of these was expected to go the other way and measured going this way, # and each has its cases placed next to the case that shows the contrast. # # 1. THE TWO RANGE DIALECTS AGREE ON EVERY EXPRESSION THEY BOTH ACCEPT. The # caret's `0.x' special case is the usual place to look for a divergence -- # `^0.9.0' -- and `caret-zero-agree' says they answer identically. So does # `~1.0.0'. The dialects differ in two other places entirely: in what they # will PARSE (npm takes hyphen ranges, space-separated conjunctions and # `||'; the simple dialect takes none of the three), and in PRERELEASES. # # 2. `Spec()' IS DOCUMENTED AS DEPRECATED AND WARNS ABOUT NOTHING. # `spec-warned' is `no'. The partial mode, two sections above it, does emit # a `DeprecationWarning' -- so the package is not simply silent about # deprecation, it is silent about this one. `spec-returns' says the type is # `LegacySpec', not `SimpleSpec', so `isinstance' tells a caller nothing # either. # # 3. A PARTIAL VERSION DOES NOT ORDER AGAINST A FULL ONE -- it raises. See # below; it is the one refusal in the package that is not a `ValueError'. # # ============================================================================ # THE PRERELEASE DIVERGENCE IS ONE CONSTRUCTOR ARGUMENT, AND `<' SWAPS SIDES. # ============================================================================ # # `Range' carries a `prerelease_policy'. `SimpleSpec' builds its ranges with # the default `PRERELEASE_NATURAL' and `NpmSpec' with `PRERELEASE_SAMEPATCH': # # NATURAL a prerelease orders like any other version, so a bound admits it # whenever the ordering does. # SAMEPATCH a prerelease is considered ONLY where the bound's own # major.minor.patch is the same; every other prerelease is refused # before the operator is reached at all. # # So the simple dialect is the permissive one -- `>=1.0.0' admits `1.5.0-rc.1' # and `2.0.0-rc.1' there and neither under npm, and `*', which reads as # "anything", is not the same set in the two. # # EXCEPT UNDER `<', WHERE THEY SWAP. `npm-lt-admits-rc' is `yes' and # `simple-lt-admits-rc' is `no': the npm dialect admits `2.0.0-rc.1' into a # range whose stated upper bound is `2.0.0', and the permissive dialect refuses # it. Both follow from the policies. Under SAMEPATCH the candidate's patch # matches the bound's, so it is considered and then simply ordered -- and it is # less than `2.0.0'. Under NATURAL, `OP_LT' carries an explicit carve-out # excluding a prerelease of the bound's OWN version, so that `<1.2.3' does not # quietly admit `1.2.3-a1'. # # A caller reading `<2.0.0' as "nothing from the 2.x line" is wrong in exactly # one dialect, and it is not the one everything else here would suggest. # # ============================================================================ # BUILD METADATA MAKES TWO VERSIONS INCOMPARABLE, AND THE PUBLIC KEY NAMED # AFTER PRECEDENCE IS NOT THE ONE PRECEDENCE USES. # ============================================================================ # # The specification says build metadata carries no order. This package obeys # it, with a consequence a caller rarely expects: `1.0.0+build.1' and # `1.0.0+build.2' are not `==' (equality DOES compare build), and are neither # `<' nor `>'. All three cases are `no' and the combination is the finding -- # any one alone is unremarkable. `compare()' reports the same thing as the # SENTINEL `NotImplemented' rather than as 0 or an exception, so a caller # testing `compare(a, b) < 0' gets a truthy object rather than an ordering. # # And there are two keys. `precedence_key' INCLUDES the build metadata; # `_cmp_precedence_key', which `__lt__' actually compares, drops it. # `build-precedence-key-eq' is `no' beside `build-cmp-key-eq' `yes' -- so a # caller who sorted on the public attribute named after precedence would order # by exactly the field the specification says has no order. # # ============================================================================ # THE PARTIAL MODE IS THIS LIBRARY'S OWN EXTENSION, AND IT RAISES `TypeError'. # ============================================================================ # # `Version(s, partial=True)' accepts an incomplete version and leaves the # missing components `None' -- it does NOT default them to zero, which is what # `partial-major-only' and `partial-major-minor' are aimed at, each against the # `strict-*' case for the same string, which is a `ValueError'. # # But a partial version DOES NOT ORDER AGAINST A FULL ONE. `__lt__' compares # `_cmp_precedence_key' element by element, a missing component is `None', and # the comparison reaches `None < int' and raises a bare `TypeError' out of the # tuple compare. `partial-lt-full' and `full-lt-partial' assert the name from # both directions; `partial-eq-full' answers `no' without raising and # `partial-lt-partial' answers `yes', so the mode is not simply broken and the # two operators disagree about whether these values are comparable at all. # # This is THE ONE REFUSAL IN THE PACKAGE THAT IS NOT A `ValueError'. A caller # who sorted a mixed list inside `except ValueError' is not protected by it. # The mode is deprecated and says so on stderr (`partial-warned'), which is the # contrast with `spec-warned' above. # # ============================================================================ # `coerce' IS LOSSY IN A WAY THAT SURVIVES AS TEXT AND VANISHES IN COMPARISON. # ============================================================================ # # A fourth numeric component is neither dropped nor an error: it is moved into # BUILD METADATA. `coerce("0.1.2.3")' is `0.1.2+3'. So the distinction those # strings were drawing survives the round trip as text and is lost to every # comparison -- `coerce-four-field-eq', `-lt' and `-gt' are all `no', i.e. the # two are INCOMPARABLE, by the rule two sections up. # # ============================================================================ # WHY THE FAILURE CASES ASSERT AN EXCEPTION NAME. # ============================================================================ # # Every refusal in this package is a `ValueError' -- a bad version and a bad # spec alike, raised from different code paths. The NAME is the whole of the # assertion: a release that let one escape as a `TypeError' or an # `AttributeError' would break every caller's `except ValueError' and change # nothing a value case could see. `version-caught-as-valueerror' asserts the # same thing from the catching side, so the names are not the only evidence. # # ============================================================================ # `RDEPENDS += python3-pkg-resources' BUYS NOTHING AT IMPORT TIME HERE. # ============================================================================ # # `__init__.py' sets `__version__' from `importlib.metadata.version()' inside a # `try:', falling back to `pkg_resources' only on `ImportError' -- i.e. only on # Python < 3.8. This image is 3.12, so the fallback is dead code and the # dependency is carried for a path that cannot be taken. It is asserted rather # than removed: the recipe is oe-core's and this port changes nothing in it, so # `pkg-depends-exact' records what the built package declares and # `dunder-version' / `metadata-version' record which route actually answered. # # ============================================================================ # THE PACKAGE SHIPS A SUBMODULE THIS IMAGE CANNOT IMPORT, AND IT DOES NOT # MATTER. # ============================================================================ # # Upstream keeps its Django integration inside the package, so # `django_fields.py' and its precompiled `.pyc' land in site-packages here. It # does `import django' at module level and this image carries no django. # `django-fields-ships' asserts the file is there and # `django-fields-not-importable' asserts what happens when you reach for it, so # a reader who found the file is told it is dead weight rather than left to # work it out. `__init__.py' does not import it, which is why `import' passes # regardless. P=python3-semantic-version # The suite reported at all. Without this, a suite that died on its first line # is indistinguishable from one that was never scheduled. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/semantic_version DI=$SITE/semantic_version-2.10.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-semantic-version.list PROBE=$BST_DATA/python3-semantic-version-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-semantic-version.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ where it lands --- # THE SHAPE IS A PACKAGE DIRECTORY of exactly three modules, not a single file. # `no-flat-module' is what would catch a release that flattened it: such a # release would still import and still pass every behavioural case below. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/no-flat-module test ! -f "$SITE/semantic_version.py" check $P/init-file test -s "$PKG/__init__.py" check $P/base-file test -s "$PKG/base.py" # The Django integration. See the header: it ships and it cannot be imported # here, and `django-fields-not-importable' below is the other half of this. check $P/django-fields-ships test -s "$PKG/django_fields.py" # THE PRECOMPILED BYTECODE, all three files. OE writes these with CHECKED_HASH # invalidation (measured off the built .deb: pyc header flags = 3), so the # source is hashed and never recompiled on target. A red here means the shipped # bytecode was rejected -- which is a statement about this arm's interpreter, # not about the package. check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-base test -s "$PKG/__pycache__/base.cpython-312.pyc" check $P/pycache-django test -s "$PKG/__pycache__/django_fields.cpython-312.pyc" # NO `py.typed'. This package ships no PEP 561 marker, unlike python3-iso8601 # beside it, so its annotations are invisible to a checker. Asserted as an # absence so a reader porting from that suite is told it is upstream's choice # rather than a packaging loss. check $P/no-py-typed test ! -e "$PKG/py.typed" # --------------------------------------------------------- what dpkg shipped --- # Copied to tmpfs first: the loop below reads it on stdin, and a top-level # redirect from the virtiofs root ends the suite outright (brintos/linux#73). # BRINTOS-WORKAROUND: brintos/linux#73 remove cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO SHARED OBJECT ANYWHERE. This image's python3 is ONE STATIC BINARY with an # empty lib-dynload and no dynamic loader (BRINTOS-1476), so a package carrying # a compiled extension could not be imported here at all. This one declares # none, and the assertion is what says the built .deb agrees with that reading. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing else either: every regular file dpkg lists is a `.py', a # `.pyc' or a dist-info member. A package that grew a data file or a console # script would show up here rather than in a behavioural red. _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 --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 2.10.0" cat "$DI/METADATA" # THE UNDERSCORE IS THE MEASURED SPELLING. PyPI's own wheel for this release # carries `Name: semantic-version' with a hyphen; the sdist this recipe builds # from carries the underscore, because setuptools took it from `setup.py' # verbatim. A reader comparing against pypi.org would otherwise read this as # wrong. check_out $P/dist-info-name "Name: semantic_version" cat "$DI/METADATA" check $P/dist-info-license test -s "$DI/LICENSE" check $P/dist-info-record test -s "$DI/RECORD" # THE BUILD BACKEND, NAMED IN THE ARTIFACT IT PRODUCED. `setuptools3' is what # this recipe inherits, and the `Generator' line is the only place on the image # that records which backend actually ran. A recipe that silently changed # backend would still install and still import; this is the case that would # notice. `top_level.txt' is a setuptools artifact and its presence says the # same thing from the other side -- python3-iso8601, built by poetry-core, has # no such file. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-top-level "semantic_version" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d '\r\n')" # ---------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-semantic-version$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 2.10.0-r0" cat "$W/st" # THE DEPENDENCY SET, EXACTLY. The recipe's `RDEPENDS' names # `python3-pkg-resources' and the packaging adds `python3-core'; both are # sub-packages of the ported python3 (BRINTOS-1476), so this port waits on no # other package. Asserting the WHOLE line rather than a substring is what would # catch a third name appearing. See the header for why the declared dependency # is not exercised at import time on this interpreter. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-pkg-resources" \ "$(cat "$W/dep")" # ------------------------------------------------------------------ it imports -- check $P/import python3 -c 'import semantic_version' # ...and SILENTLY. A module that imports but warns on the way is a different # result from one that imports clean, and `check' above would pass on both -- # it reads the exit status, and a warning goes to stderr without touching it. # It matters more here than in most of these suites: this package emits a # `DeprecationWarning' from the partial mode, so a warning at IMPORT would be # easy to mistake for that one. check_eq $P/import-quiet "" \ "$(python3 -c 'import semantic_version' 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 START for everything below. 900s is the bound on the start, not on the # work: see the BST-TIMEOUT note at the top. 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 TRANSCRIPT IS WHOLE. Without these two a probe that died two thirds of the # way through would report every case it had reached as a pass and the rest as # an empty-string mismatch, which reads as a behavioural regression rather than # as a truncation. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "157" "$(pv emitted_count)" # --------------------------------------------------------------- the identity --- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here; without these they would be statements about whatever was # first on sys.path. check_eq $P/package-answered "$PKG/__init__.py" "$(pv package_file)" check_eq $P/base-answered "$PKG/base.py" "$(pv base_file)" check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION BY TWO ROUTES, asserted to agree. See the header: `__version__' # comes from `importlib.metadata' on this interpreter and never touches # `pkg_resources'. A disagreement between these two would mean two installs. check_eq $P/dunder-version "2.10.0" "$(pv dunder_version)" check_eq $P/metadata-version "2.10.0" "$(pv metadata_version)" # NON-ASCII, ESCAPED. The author's name carries an `e' with an acute accent, so # the probe reports it through `ascii()' -- which escapes it -- rather than # through `repr()', which in Python 3 would leave a printable non-ASCII # character exactly as it is. That keeps this a statement about the package # instead of one about the target's stdout encoding. check_eq $P/author-ascii \ "'Rapha\\xebl Barrois '" \ "$(pv author_ascii)" # THE EXPORTED SURFACE. There is no `__all__' -- `__init__.py' names its # imports on one `from .base import' line -- so that line IS the public API, and # it is asserted name by name. for _n in compare match validate SimpleSpec NpmSpec Spec SpecItem Version; do check_eq $P/has-$_n "yes" "$(pv has_$_n)" done # -------------------------------------------------- every field, once, apart --- # ONE INPUT WITH NO TWO FIELDS ALIKE -- 1.2.3-rc.4+build.5 -- so a transposition # is visible. Asserted field by field rather than as one joined string: a joined # line reds as a single case whichever field moved, and these five are the whole # of what a `Version' holds. check_eq $P/full-major "1" "$(pv full_major)" check_eq $P/full-minor "2" "$(pv full_minor)" check_eq $P/full-patch "3" "$(pv full_patch)" check_eq $P/full-prerelease "rc,4" "$(pv full_prerelease)" check_eq $P/full-build "build,5" "$(pv full_build)" check_eq $P/full-str "1.2.3-rc.4+build.5" "$(pv full_str)" check_eq $P/full-partial "no" "$(pv full_partial)" # TUPLES OF STRINGS, not one string and not a list. A caller who indexed # `v.prerelease[0]' expecting `'r'' has a type error no value case can see. check_eq $P/prerelease-type "tuple" "$(pv prerelease_type)" check_eq $P/build-type "tuple" "$(pv build_type)" check_eq $P/prerelease-item-type "str" "$(pv prerelease_item_type)" # ----------------------------------------------------------- the round trip --- # EVERY DOCUMENTED SHAPE, parsed and printed back. The assertion is that the # output is the INPUT, byte for byte: this package normalises nothing, so a # release that started dropping a `+build' or zero-padding a field would show up # here and nowhere else. check_eq $P/round-plain "1.2.3" "$(pv round_plain)" check_eq $P/round-zeros "0.0.0" "$(pv round_zeros)" check_eq $P/round-big "10.20.30" "$(pv round_big)" check_eq $P/round-pre-alpha "1.0.0-alpha" "$(pv round_pre_alpha)" check_eq $P/round-pre-dotted "1.0.0-alpha.1" "$(pv round_pre_dotted)" check_eq $P/round-pre-numeric "1.0.0-0.3.7" "$(pv round_pre_numeric)" check_eq $P/round-pre-hyphen "1.0.0-x-y-z.-" "$(pv round_pre_hyphen)" check_eq $P/round-build-only "1.0.0+20130313144700" "$(pv round_build_only)" check_eq $P/round-build-dotted "1.0.0+exp.sha.5114f85" "$(pv round_build_dotted)" check_eq $P/round-both "1.0.0-beta+exp.sha.5114f85" "$(pv round_both)" # ---------------------------------------------------------------- precedence --- # THE ORDERING THE SPECIFICATION FIXES, as one sorted list -- semver.org's own # worked example, extended with the two rules a caller is most likely to get # wrong. Asserted as a list rather than as pairs because the finding is the # ORDER, and a pair-at-a-time reading cannot show a rule applying across the # whole set. # # READ THE FIRST TWO ENTRIES AND THE PLACE `1.0.0' FALLS. `1.0.0-2' before # `1.0.0-11' is NUMERIC comparison of a numeric identifier, not the string # order; both before `1.0.0-alpha' is the rule that a numeric identifier sorts # below an alphanumeric one; and `1.0.0' itself sorts ABOVE all nine of its own # prereleases, which is the opposite of a plain string compare. check_eq $P/sorted-versions \ "1.0.0-2,1.0.0-11,1.0.0-alpha,1.0.0-alpha.1,1.0.0-alpha.beta,1.0.0-beta,1.0.0-beta.2,1.0.0-beta.11,1.0.0-rc.1,1.0.0,1.0.1,1.1.0,2.0.0" \ "$(pv sorted_versions)" # BUILD METADATA MAKES TWO VERSIONS INCOMPARABLE. See the header: the three # `no's TOGETHER are the finding, and any one of them alone is unremarkable. check_eq $P/build-eq "no" "$(pv build_eq)" check_eq $P/build-lt "no" "$(pv build_lt)" check_eq $P/build-gt "no" "$(pv build_gt)" # THE PUBLIC KEY NAMED AFTER PRECEDENCE IS NOT THE ONE PRECEDENCE USES. These # two disagree by design and the pair is the statement; see the header. check_eq $P/build-precedence-key-eq "no" "$(pv build_precedence_key_eq)" check_eq $P/build-cmp-key-eq "yes" "$(pv build_cmp_key_eq)" # ...and against the same version with NO build at all, which is the pair that # catches a caller using `==' to mean "the same release". check_eq $P/build-vs-none-eq "no" "$(pv build_vs_none_eq)" check_eq $P/build-vs-none-lt "no" "$(pv build_vs_none_lt)" check_eq $P/build-vs-none-gt "no" "$(pv build_vs_none_gt)" # `compare' IS THE MODULE-LEVEL SHORTHAND, strings in and the C convention out. # `compare-build-zero' is the one to read: given two versions differing only in # build metadata it returns the SENTINEL `NotImplemented' -- not 0, and not an # exception. A caller testing `compare(a, b) < 0' gets a truthy object rather # than an ordering, so the incomparability above leaks out of the operators and # into this function's return value. check_eq $P/compare-lt "-1" "$(pv compare_lt)" check_eq $P/compare-gt "1" "$(pv compare_gt)" check_eq $P/compare-eq "0" "$(pv compare_eq)" check_eq $P/compare-pre-lt "-1" "$(pv compare_pre_lt)" check_eq $P/compare-build-zero "NotImplemented" "$(pv compare_build_zero)" # ------------------------------------------------------------ the partial mode --- # `None', NOT ZERO, for every component the input left out -- which is the # whole point of the mode and the mistake these cases are aimed at. The format # is `major.minor.patch|prerelease|build'. check_eq $P/partial-major-only "1.None.None|None|None" "$(pv partial_major_only)" check_eq $P/partial-major-minor "1.2.None|None|None" "$(pv partial_major_minor)" check_eq $P/partial-full "1.2.3|None|None" "$(pv partial_full)" check_eq $P/partial-with-pre "1.2.3|rc.1|None" "$(pv partial_with_pre)" # A TRAILING `-' IS AN EMPTY PRERELEASE, not an absent one -- the empty field # between the pipes -- and the strict parser rejects the same string outright # (`err-version-empty-pre'). It is the sharpest case for `None' and `()' being # different answers. check_eq $P/partial-trailing-dash "1.2.3||None" "$(pv partial_trailing_dash)" # The same strings WITHOUT the flag. Without this pair the cases above would # read as "the parser is lenient" rather than as "a second mode exists". check_eq $P/strict-major-only "ValueError" "$(pv strict_major_only)" check_eq $P/strict-major-minor "ValueError" "$(pv strict_major_minor)" check_eq $P/partial-str "1.2" "$(pv partial_str)" check_eq $P/partial-flag "yes" "$(pv partial_flag)" # IT RAISES RATHER THAN ORDERING. See the header -- the one refusal here that # is not a `ValueError'. Asserted from both directions, because either alone # could be read as an ordering result rather than as a raise. check_eq $P/partial-lt-full "TypeError" "$(pv partial_lt_full)" check_eq $P/full-lt-partial "TypeError" "$(pv full_lt_partial)" # ...while `==' answers without raising, and two partials of the same shape DO # order. Together these say the mode is not simply broken and that the two # operators disagree about whether these values are comparable at all. check_eq $P/partial-eq-full "no" "$(pv partial_eq_full)" check_eq $P/partial-lt-partial "yes" "$(pv partial_lt_partial)" # THE MODE IS DEPRECATED AND SAYS SO -- exactly once per construction, on # stderr, naming `SimpleSpec('1.x.x')' as the replacement. This is the contrast # with `spec-warned' below, where the documentation says deprecated and the code # says nothing. check_eq $P/partial-warned "yes" "$(pv partial_warned)" check_eq $P/partial-warning-count "1" "$(pv partial_warning_count)" # `coerce' IS A THIRD MODE, and a lossy one. `coerce-four-field' is the case to # read: the fourth component becomes BUILD METADATA. See the header. check_eq $P/coerce-two-field "2.0.0" "$(pv coerce_two_field)" check_eq $P/coerce-three-field "2.1.0" "$(pv coerce_three_field)" check_eq $P/coerce-four-field "0.1.2+3" "$(pv coerce_four_field)" check_eq $P/coerce-trailing "1.2.3+4-rc.1" "$(pv coerce_trailing)" check_eq $P/coerce-already "1.2.3" "$(pv coerce_already)" # ...and the three that say what that costs: `0.1.2.3' and `0.1.2.4' are not # equal and are not ordered either. check_eq $P/coerce-four-field-eq "no" "$(pv coerce_four_field_eq)" check_eq $P/coerce-four-field-lt "no" "$(pv coerce_four_field_lt)" check_eq $P/coerce-four-field-gt "no" "$(pv coerce_four_field_gt)" # -------------------------------------------------------- the range language --- # ONE FIXED CANDIDATE LIST for every case below, in this order: # # 0.9.0 0.9.1 1.0.0-alpha 1.0.0-beta 1.0.0 1.0.1 1.1.0 # 1.5.0-rc.1 1.5.0 1.9.9 2.0.0-rc.1 2.0.0 2.1.0 3.0.0 # # The admitted set is emitted in that order, so each case states both WHICH # versions matched and that filtering does not reorder. `select()' returns the # HIGHEST admitted candidate, not the first, which is why it is asserted beside # every match line rather than once. check_eq $P/simple-gte-match \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_gte_match)" check_eq $P/simple-gte-select "3.0.0" "$(pv simple_gte_select)" check_eq $P/simple-gt-match \ "1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_gt_match)" check_eq $P/simple-gt-select "3.0.0" "$(pv simple_gt_select)" # `<2.0.0' EXCLUDES `2.0.0-rc.1' HERE and admits `1.5.0-rc.1'. That is the # `OP_LT' carve-out from the header, and it is the case the npm dialect # contradicts. check_eq $P/simple-lt-match \ "0.9.0,0.9.1,1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" \ "$(pv simple_lt_match)" check_eq $P/simple-lt-select "1.9.9" "$(pv simple_lt_select)" check_eq $P/simple-lte-match \ "0.9.0,0.9.1,1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0" \ "$(pv simple_lte_match)" check_eq $P/simple-lte-select "1.1.0" "$(pv simple_lte_select)" check_eq $P/simple-eq-match "1.0.0" "$(pv simple_eq_match)" check_eq $P/simple-eq-select "1.0.0" "$(pv simple_eq_select)" check_eq $P/simple-ne-match \ "0.9.0,0.9.1,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_ne_match)" check_eq $P/simple-ne-select "3.0.0" "$(pv simple_ne_select)" # A BARE VERSION IS ACCEPTED and means `=='. Asserted because the neighbouring # `simple-refuses-*' cases could otherwise be read as "this dialect refuses # anything without an operator", which is not what it does. check_eq $P/simple-bare-match "1.0.0" "$(pv simple_bare_match)" check_eq $P/simple-bare-select "1.0.0" "$(pv simple_bare_select)" # `~' IS THE PATCH RANGE and `^' the minor range -- the two spellings a caller # most often reaches for and most often swaps. Read them together: `~1.0.0' # stops at `1.0.1' and `^1.0.0' runs to `1.9.9'. check_eq $P/simple-tilde-match "1.0.0,1.0.1" "$(pv simple_tilde_match)" check_eq $P/simple-tilde-select "1.0.1" "$(pv simple_tilde_select)" check_eq $P/simple-caret-match \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" "$(pv simple_caret_match)" check_eq $P/simple-caret-select "1.9.9" "$(pv simple_caret_select)" # `^0.9.0' IS THE CARET'S `0.x' SPECIAL CASE -- below 1.0.0 a caret is a PATCH # range, not a minor one, because a `0.x' minor bump is allowed to break. This # is the expression the two dialects were expected to disagree on and do not; # see `caret-zero-agree'. check_eq $P/simple-caret-zero-match "0.9.0,0.9.1" "$(pv simple_caret_zero_match)" check_eq $P/simple-caret-zero-select "0.9.1" "$(pv simple_caret_zero_select)" check_eq $P/simple-wild-minor-match \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" "$(pv simple_wild_minor_match)" check_eq $P/simple-wild-minor-select "1.9.9" "$(pv simple_wild_minor_select)" check_eq $P/simple-wild-patch-match "1.0.0,1.0.1" "$(pv simple_wild_patch_match)" check_eq $P/simple-wild-patch-select "1.0.1" "$(pv simple_wild_patch_select)" # `*' ADMITS EVERY CANDIDATE INCLUDING ALL FOUR PRERELEASES, which is the # reading its spelling suggests -- and is NOT what the npm dialect does with the # same string. See `npm-pre-star'. check_eq $P/simple-wild-all-match \ "0.9.0,0.9.1,1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_wild_all_match)" check_eq $P/simple-wild-all-select "3.0.0" "$(pv simple_wild_all_select)" # COMMA IS THE CONJUNCTION, and it is the ONLY one this dialect has -- see # `simple-refuses-space-and'. `and-three' adds an exclusion to show the clauses # compose rather than merely being bounds. check_eq $P/simple-and-match \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" "$(pv simple_and_match)" check_eq $P/simple-and-select "1.9.9" "$(pv simple_and_select)" check_eq $P/simple-and-three-match \ "1.0.0,1.0.1,1.5.0-rc.1,1.5.0,1.9.9" "$(pv simple_and_three_match)" check_eq $P/simple-and-three-select "1.9.9" "$(pv simple_and_three_select)" # A SPEC THAT ADMITS NOTHING: `select' returns `None' rather than raising. This # is the only way a caller learns the difference between "no candidate matched" # and "the spec was bad" -- the latter is a `ValueError' on construction, below. check_eq $P/simple-empty-match "" "$(pv simple_empty_match)" check_eq $P/simple-empty-select "None" "$(pv simple_empty_select)" # ----------------------------------------- where the two dialects do NOT differ --- # THE OBVIOUS GUESS, CHECKED AND WRONG. See finding 1 in the header: for every # expression both dialects accept they admit the identical set, the caret's # `0.x' special case included. Asserted directly so a reader is told the guess # was made and measured rather than left to assume it was never considered. check_eq $P/caret-zero-agree "yes" "$(pv caret_zero_agree)" check_eq $P/tilde-agree "yes" "$(pv tilde_agree)" # ---------------------------------------------- where they DO differ: prereleases --- # ONE CONSTRUCTOR ARGUMENT, asserted as six pairs. See the header for the two # policies. Both halves of each pair are asserted rather than one difference, # because the finding is WHICH dialect does what and a single "they differ" # case would not say. check_eq $P/simple-pre-gte \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_pre_gte)" check_eq $P/npm-pre-gte \ "1.0.0,1.0.1,1.1.0,1.5.0,1.9.9,2.0.0,2.1.0,3.0.0" "$(pv npm_pre_gte)" check_eq $P/simple-pre-gt \ "1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_pre_gt)" check_eq $P/npm-pre-gt \ "1.0.1,1.1.0,1.5.0,1.9.9,2.0.0,2.1.0,3.0.0" "$(pv npm_pre_gt)" # THE PAIR WHERE THEY SWAP SIDES. npm keeps `2.0.0-rc.1' under `<2.0.0' and # drops `1.0.0-alpha'/`1.0.0-beta'/`1.5.0-rc.1'; the simple dialect does the # exact reverse. See the header. check_eq $P/simple-pre-lt \ "0.9.0,0.9.1,1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" \ "$(pv simple_pre_lt)" check_eq $P/npm-pre-lt \ "0.9.0,0.9.1,1.0.0,1.0.1,1.1.0,1.5.0,1.9.9,2.0.0-rc.1" "$(pv npm_pre_lt)" check_eq $P/simple-pre-caret \ "1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9" "$(pv simple_pre_caret)" check_eq $P/npm-pre-caret \ "1.0.0,1.0.1,1.1.0,1.5.0,1.9.9" "$(pv npm_pre_caret)" check_eq $P/simple-pre-star \ "0.9.0,0.9.1,1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_pre_star)" check_eq $P/npm-pre-star \ "0.9.0,0.9.1,1.0.0,1.0.1,1.1.0,1.5.0,1.9.9,2.0.0,2.1.0,3.0.0" "$(pv npm_pre_star)" # A BOUND THAT NAMES A PRERELEASE ITSELF. Under both policies this admits the # prereleases of THAT version -- `1.0.0-alpha' and `1.0.0-beta' appear in both # lines -- and the dialects still differ over every OTHER version's prereleases, # which is what says SAMEPATCH is per-version rather than a global switch. check_eq $P/simple-pre-pre-bound \ "1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0-rc.1,1.5.0,1.9.9,2.0.0-rc.1,2.0.0,2.1.0,3.0.0" \ "$(pv simple_pre_pre_bound)" check_eq $P/npm-pre-pre-bound \ "1.0.0-alpha,1.0.0-beta,1.0.0,1.0.1,1.1.0,1.5.0,1.9.9,2.0.0,2.1.0,3.0.0" \ "$(pv npm_pre_pre_bound)" # THE SWAP, STATED AS ONE PAIR. The two lines above carry it inside a # fourteen-element list; these two say it on its own, which is what a reader # takes away. check_eq $P/npm-lt-admits-rc "yes" "$(pv npm_lt_admits_rc)" check_eq $P/simple-lt-admits-rc "no" "$(pv simple_lt_admits_rc)" # --------------------------------- and where they differ: the grammar itself --- # npm's own range syntax -- a hyphen range, a SPACE-separated conjunction, and # `||' -- is not the simple dialect's, which takes only comma-joined # comparators. Each is asserted from both sides: a refusal alone does not say # the other dialect accepts it, and an acceptance alone does not say the first # refuses. check_eq $P/simple-refuses-hyphen "ValueError" "$(pv simple_refuses_hyphen)" check_eq $P/npm-hyphen-match \ "1.0.0,1.0.1,1.1.0,1.5.0,1.9.9,2.0.0" "$(pv npm_hyphen_match)" check_eq $P/simple-refuses-space-and "ValueError" "$(pv simple_refuses_space_and)" check_eq $P/npm-space-and-match \ "1.0.0,1.0.1,1.1.0,1.5.0,1.9.9" "$(pv npm_space_and_match)" # `||' IS A DISJUNCTION, which the simple dialect has no spelling for at all -- # every clause there is conjunctive. So this is not a syntax difference alone: # there are sets the simple dialect cannot express. check_eq $P/simple-refuses-or "ValueError" "$(pv simple_refuses_or)" check_eq $P/npm-or-match "1.0.0,3.0.0" "$(pv npm_or_match)" # --------------------------------------------------------- the module helpers --- # `validate' RETURNS A BOOL AND NEVER RAISES, which is what makes it usable as a # gate where `Version()' is not. `validate-partial' is the case to read: a # partial version is NOT valid, so this function answers about the strict mode # only and cannot gate the partial one. check_eq $P/validate-full "yes" "$(pv validate_full)" check_eq $P/validate-with-pre "yes" "$(pv validate_with_pre)" check_eq $P/validate-with-build "yes" "$(pv validate_with_build)" check_eq $P/validate-partial "no" "$(pv validate_partial)" check_eq $P/validate-leading-zero "no" "$(pv validate_leading_zero)" check_eq $P/validate-empty "no" "$(pv validate_empty)" check_eq $P/validate-junk "no" "$(pv validate_junk)" # `match' IS `Version in SimpleSpec' SPELLED AS A FUNCTION -- strings in, bool # out -- the shorthand a caller reaches for before discovering the objects. check_eq $P/match-in "yes" "$(pv match_in)" check_eq $P/match-out "no" "$(pv match_out)" check_eq $P/match-caret "yes" "$(pv match_caret)" # ---------------------------------------------------------- the failure path --- # EVERY REFUSAL IS A `ValueError'. See the header for why the NAME is the whole # of the assertion. The nine version cases cover the grammar's edges: a leading # zero, a negative, too few and too many components, an empty prerelease, an # underscore in a prerelease identifier, and a trailing dot. # Spelled as id/key pairs rather than derived with `tr': the case id and the # probe key differ by separator, and a `tr' set whose first character is `-' is # ambiguous with an option on the target's ash -- garbled ids for no benefit. for _p in empty:empty junk:junk leading-zero:leading_zero negative:negative \ too-few:too_few too-many:too_many empty-pre:empty_pre \ bad-pre-char:bad_pre_char trailing-dot:trailing_dot; do check_eq $P/err-version-${_p%%:*} "ValueError" "$(pv err_version_${_p#*:})" done # ...and the spec side, raised from a different code path. `bad-version' is the # one worth naming: a spec whose OPERAND is not a version fails on the same # exception as a spec whose operator is unknown, so a caller cannot tell the two # apart by type. for _p in empty:empty junk:junk bad-op:bad_op dangling:dangling \ bad-version:bad_version; do check_eq $P/err-spec-${_p%%:*} "ValueError" "$(pv err_spec_${_p#*:})" done check_eq $P/err-npm-junk "ValueError" "$(pv err_npm_junk)" # Asserted once from the CATCHING side, so the names above are not the only # evidence that a built-in `except ValueError' works. check_eq $P/version-caught-as-valueerror "ValueError" \ "$(pv version_caught_as_valueerror)" # ------------------------------------------------- the deprecated legacy names --- # See finding 2 in the header. `Spec()' still works -- `spec-equivalent' -- and # says nothing at all about being deprecated: no warning, and a `LegacySpec' # type that `isinstance(s, SimpleSpec)' does not recognise. Read `spec-warned' # beside `partial-warned' above, which is the same package doing the opposite # for its other deprecated API. check_eq $P/spec-returns "LegacySpec" "$(pv spec_returns)" check_eq $P/spec-warned "no" "$(pv spec_warned)" check_eq $P/spec-equivalent "yes" "$(pv spec_equivalent)" # ----------------------------- the submodule that ships and cannot be imported --- # See the header. `django-fields-ships' above asserted the file is there; this # says what happens when you reach for it, and that it is harmless -- # `__init__.py' does not import it, so `import' passed regardless. check_eq $P/django-fields-not-importable "ModuleNotFoundError" \ "$(pv django_fields_imports)" rm -rf "$W"