. "$BST_LIB" # BST-TIMEOUT: 1500 # FOUR python3 starts, and they are the whole cost. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps one probe transcript. # # # # The four: a bare `import semver', a second import whose # # STDERR is the assertion, the probe -- ONE start for 316 # # measurements -- and the shipped `pysemver' program, run # # as a real process because a shebang and an exit status # # cannot be measured any other way. # # # # The work inside a start is a few hundred `re' matches # # over strings under 40 bytes plus integer and string # # comparisons, which measures 0.021s for the whole probe on # # the x86-64 control. Microseconds beside an interpreter # # start on this arm, which is tens of seconds before the # # first line of the program runs. # # # # 1500 is 375s per start, against the ~60s a python3 start # # has measured at here. The start dominates and nothing in # # this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-semver 3.0.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-semver_3.0.2.bb, `inherit pypi # python_setuptools_build_meta ptest', PyPI sdist) on the wasm32-hwjs glibc # image. # # ============================================================================ # THIS PACKAGE IMPLEMENTS A SPECIFICATION, AND THAT CHANGES WHAT A RED MEANS. # ============================================================================ # # Semantic Versioning 2.0.0 is a published document (semver.org) whose entire # content is a grammar and a precedence order. So unlike a parser whose # behaviour is its author's taste, most cases below have an EXTERNAL AUTHORITY: # clause 11 decides what `1.0.0-alpha' compares against `1.0.0', and a red is a # divergence from a document rather than from this port's opinion. The clause # number is named above each block, so a red can be adjudicated by reading the # spec instead of reading semver's source. # # The cases that have NO such authority are marked as such -- `bumppre_*', # `next_*', `compat_*', the constructor and the whole deprecated-2.x block are # semver's own choices, and the point of pinning them is that a release may # change its mind. Keeping the two kinds visibly apart is the reason this file # is organised by clause rather than by method name. # # ============================================================================ # THE THREE FINDINGS. These are what the port measured that the docs do not say. # ============================================================================ # # 1. THE CONSTRUCTOR DOES NOT VALIDATE. `Version.parse' refuses eighteen shapes # below; `Version(...)' checks exactly one thing -- that the numbers are not # negative -- and stores everything else as given. So # `Version(1, 0, 0, prerelease="a_b")' is accepted, renders `1.0.0-a_b', and # `Version.parse' of that string RAISES. The library produces a version it # refuses to read back, through its own public API, with no error anywhere. # `replace' inherits it, which is what makes it a caller's problem rather # than a curiosity: `replace' is the documented way to change one field. # # 2. `bump_prerelease' IS A SILENT NO-OP on a prerelease with no digits in it. # The implementation builds the new prerelease and then increments the LAST # RUN OF DIGITS anywhere in the string; `alpha' has none, so # `Version.parse("1.2.3-alpha").bump_prerelease()' returns `1.2.3-alpha' # unchanged. A release script that bumps once and publishes overwrites the # release it just made. # # 3. THE DEPRECATION CATEGORY IS NOT UNIFORM. The 2.x module-level functions # are wrapped in a decorator that warns, but `compare' and `match' carry a # PendingDeprecationWarning while `parse' and the bumps carry a plain # DeprecationWarning -- because whether the module-level comparison API # survives at all is still open upstream. `-W error::DeprecationWarning' # turns one set into failures and leaves the other running. # # All three are UPSTREAM's, in a third-party package. This project files nothing # against third-party public repositories, so they are pinned here as what 3.0.2 # does; a release that fixes any of them reds in this file and says which. # # ============================================================================ # WHY THE UPSTREAM ptest SUITE IS NOT RUN, and why that is not a gap. # ============================================================================ # # The recipe does `inherit ptest' and ships a run-ptest that calls `pytest # --automake'. It produces NO -ptest package here, and the reason is measured # rather than assumed: this distro sets DISTRO_FEATURES without "ptest" # (conf/distro/brintos.conf), so ptest.bbclass never adds ${PN}-ptest to # PACKAGES at all. The recipe-scope build produced python3-semver, # python3-semver-dev and python3-semver-dbg and no -ptest deb -- the same # mechanism already recorded for attr, acl and python3-wcwidth in the image # recipe. Forcing one would put python3-pytest and # python3-unittest-automake-output on the image, and pytest's own RDEPENDS names # packages that are not ported and are not this ticket's. # # What survives that decision is upstream's DATA. `tests/test_parsing.py' and # `tests/test_compare.py' carry fixture TABLES of valid and invalid version # strings, and a table is portable even when the runner that reads it is not -- # so the probe carries 31 strings the author says must parse and 35 the author # says must not, and runs them against this image's interpreter. That is the # author's statement about the implementation, which is evidence of a different # kind from this port's hand-written cases. # # ============================================================================ # WHAT THE PROBE IS FOR. # ============================================================================ # # A python3 start on this arm is tens of seconds before the first line of a # program runs, and this suite has 316 things to say about semver. As separate # `python3 -c' invocations that would be hours of interpreter starts; as one # probe it is a single start, and every case below is a `grep' over its # transcript. `probe_complete' and `emitted_count' are what make a got:'' below # readable as a missing KEY rather than as a probe that died half way. P=python3-semver # ------------------------------------------------------------------ canary --- # The suite ran at all. Emitted before anything that could exit early, so a file # that produced nothing is distinguishable from one that produced only reds. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/semver DI=$SITE/semver-3.0.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-semver.list PROBE=$BST_DATA/python3-semver-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-semver.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ----------------------------------------------------------------- payload --- # WHAT THE .deb PUT ON THE IMAGE, asserted before anything is imported -- so a # behavioural red further down can be told apart from a package that did not # land. Measured against the built deb: five .py files, their .pyc, a marker # file, a six-file dist-info and one executable. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/package-init test -s "$PKGDIR/__init__.py" # A PACKAGE, NOT A FLAT MODULE. python3-docopt ships one .py; semver ships a # directory. A release that flattened it would still import and pass every # behavioural case, so the shape is asserted directly. check $P/no-flat-module test ! -f "$SITE/semver.py" for _m in __about__ __main__ _deprecated _types cli version; do check $P/module-$_m test -s "$PKGDIR/$_m.py" done check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-version test -s "$PKGDIR/__pycache__/version.cpython-312.pyc" # `py.typed` IS THE ONE NON-PYTHON FILE IN THE PAYLOAD, and it is EMPTY by # design: PEP 561 makes the marker's PRESENCE the whole of its content, which is # what tells a type checker the inline annotations are authoritative. It is # asserted with `test -f` rather than `test -s` for exactly that reason -- an # empty file is correct here and would fail the usual check. check $P/py-typed test -f "$PKGDIR/py.typed" check_eq $P/py-typed-empty "0" "$(wc -c < "$PKGDIR/py.typed" 2>/dev/null | tr -d ' ')" # THIS PACKAGE SHIPS AN EXECUTABLE, which isodate and docopt do not: setup.cfg # declares a `console_scripts' entry point, so the recipe generates a program in # ${bindir} with a shebang. It is asserted as a file here and RUN as a process # further down, which are two different statements. check $P/bin-pysemver test -x /usr/bin/pysemver # THE dpkg FILE LIST IS THE PACKAGING'S OWN STATEMENT, read separately from the # filesystem above so that a file present but unowned, or owned but absent, is # visible as a disagreement between the two. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO SHARED OBJECT. This image's python3 is one static binary with an empty # lib-dynload and no dynamic loader (BRINTOS-1476), so a module carrying a # compiled extension could not be imported at all. semver's WHEEL says # `Root-Is-Purelib: true'; this is the packaging saying the same thing. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and the payload is python plus exactly two known exceptions: the PEP 561 # marker and the console script. Listing them rather than widening the filter is # what keeps the case able to catch a third. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */semver/py.typed) continue ;; /usr/bin/pysemver) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" check_out $P/pkg-owns-script "/usr/bin/pysemver" cat "$W/pkgf" # THE dist-info IS THE INSTALLED PACKAGING'S VERSION STATEMENT, and the probe # cross-checks it against the module's own `__version__' -- two readings that # must agree, which is what catches a recipe built from a tree that does not # match its PV. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 3.0.2" cat "$DI/METADATA" check_out $P/dist-info-top-level "semver" cat "$DI/top_level.txt" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # THE ENTRY POINT IS DECLARED IN THE INSTALLED METADATA TOO, not only in the # generated script -- so a caller enumerating console scripts finds it. check_out $P/dist-info-entry-point "pysemver = semver.cli:main" cat "$DI/entry_points.txt" # ...AND THE LICENCE IS SHIPPED, which isodate's dist-info does not do. Stated # rather than left absent: BSD-3-Clause is what the recipe declares, and a # binary distribution of it has to carry the text. check $P/dist-info-license test -s "$DI/LICENSE.txt" # THE dpkg STANZA. `Depends' is the assertion that matters: python3-core and # NOTHING ELSE, which is why this port waited on no other ticket. The probe's # `import_nonstdlib' says the same thing from the interpreter's side. sed -n '/^Package: python3-semver$/,/^$/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" 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" # ...and NO OTHER python3-* package. Asserted as the absence of a second # python3 dependency rather than as the exact text of the line, which carries # whatever version constraints the packaging chose to attach: the fact this port # rests on is that nothing THIRD-PARTY is depended on, and that is what an extra # `python3-` in this line would say. check_eq $P/pkg-depends-only-core "" \ "$(tr ', ' '\n\n' < "$W/dep" 2>/dev/null | grep '^python3-' | grep -v '^python3-core$' | tr '\n' ' ')" # NO -ptest PACKAGE IS INSTALLED, and this is an assertion rather than a # silence. The recipe inherits ptest; a DISTRO_FEATURES with no `ptest' is why # none was produced. A future image that grew one should RUN it rather than keep # shipping the reproduction of its data in the probe, and this case is what says # so. check_eq $P/no-ptest-package "" \ "$(sed -n 's/^Package: \(python3-semver-ptest\)$/\1/p' /var/lib/dpkg/status 2>/dev/null)" # ------------------------------------------------------------------ import --- # THE IMPORT IS ITS OWN CASE, before the probe, so an import failure has one red # naming its cause rather than 300 naming symptoms. check $P/import python3 -c 'import semver' # ...AND IT IS SILENT. A module that imports but writes to stderr -- a # DeprecationWarning from its own internals, a missing-optional notice -- is a # different fault from one that fails, and only this case can see it. It matters # here in particular: this package's whole 2.x surface is wrapped in warning # decorators, and the assertion is that importing does not FIRE any of them. check_eq $P/import-quiet "" \ "$(python3 -c 'import semver' 2>&1)" # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE TWO CASES THAT MAKE EVERY OTHER ONE READABLE. Without them a probe that # died two thirds through leaves the early cases green and the rest reading # got:'', which looks like a partial library rather than a partial run. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "317" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/submodule-count "5" "$(pv submodule_count)" check_eq $P/submodules "__about__,_deprecated,_types,cli,version" "$(pv submodules)" # THREE VERSION READINGS, TWO OF WHICH MUST AGREE. `SEMVER_SPEC_VERSION' is the # version of the SPECIFICATION, not of the package -- 2.0.0 against 3.0.2 -- and # confusing the two is the reason it is asserted beside them rather than alone. check_eq $P/dunder-version "3.0.2" "$(pv dunder_version)" check_eq $P/dist-version "3.0.2" "$(pv dist_version)" check_eq $P/version-agrees "yes" "$(pv version_agrees)" check_eq $P/spec-version "2.0.0" "$(pv spec_version)" check_eq $P/all-count "30" "$(pv all_count)" check_eq $P/all-sorted-head "SEMVER_SPEC_VERSION,Version,VersionInfo,__author__,__author_email__" "$(pv all_sorted_head)" for _n in Version VersionInfo parse compare match bump_major bump_minor \ bump_patch bump_prerelease bump_build finalize_version \ format_version replace max_ver min_ver parse_version_info \ SEMVER_SPEC_VERSION; do check_eq $P/has-$(echo "$_n" | tr '_' '-') "yes" "$(pv has_$_n)" done # VersionInfo IS Version, by assignment -- not a subclass. A release that made # it a real subclass would change what `type(v) is Version' answers for code # that has been written against either name since 2.x. check_eq $P/versioninfo-is-version "yes" "$(pv versioninfo_is_version)" check_eq $P/all-module-files-py "yes" "$(pv all_module_files_py)" # THE RECIPE'S EMPTY TARGET RDEPENDS, RESTATED FROM THE INTERPRETER'S SIDE: # importing semver pulled in nothing outside the standard library. This is the # strict assertion, and it holds whatever the interpreter loaded for its own # reasons. check_eq $P/import-nonstdlib "" "$(pv import_nonstdlib)" # ...and the same delta from the library's side: the three modules semver's own # sources import at module scope were all pulled. The FULL delta is emitted # beside this as `import_pulled' and read from the probe output, not asserted -- # it carries the stdlib's transitive bookkeeping, which differs per interpreter # build. Asserting it exactly is what this case used to do, and wasm32-hwjs # pulled `_weakrefset', `importlib' and `warnings' on top of the x86-64 # control's eleven, which is the interpreter answering differently and not # semver doing anything. check_eq $P/import-pulled-missing "" "$(pv import_pulled_missing)" # ------------------------------------------------------------------- parse --- # CLAUSE 2 (the triple), 9 (prerelease) and 10 (build). Every fixture asserts # all five fields SEPARATELY and then the round trip: a parser that put the # build metadata in `prerelease' would render back to the same string and pass a # str()-only test. _fx() { # name major minor patch prerelease build roundtrip check_eq $P/f-$1-major "$2" "$(pv f_$(echo "$1" | tr '-' '_')_major)" check_eq $P/f-$1-minor "$3" "$(pv f_$(echo "$1" | tr '-' '_')_minor)" check_eq $P/f-$1-patch "$4" "$(pv f_$(echo "$1" | tr '-' '_')_patch)" check_eq $P/f-$1-pre "$5" "$(pv f_$(echo "$1" | tr '-' '_')_pre)" check_eq $P/f-$1-build "$6" "$(pv f_$(echo "$1" | tr '-' '_')_build)" check_eq $P/f-$1-rt "$7" "$(pv f_$(echo "$1" | tr '-' '_')_rt)" } _fx plain 1 2 3 None None 1.2.3 _fx zeroes 0 0 0 None None 0.0.0 _fx pre 1 2 3 "'alpha'" None 1.2.3-alpha _fx pre-num 1 2 3 "'alpha.1'" None 1.2.3-alpha.1 _fx build 1 2 3 None "'build.11'" 1.2.3+build.11 _fx pre-build 1 2 3 "'alpha.beta'" "'exp.sha.5114f85'" 1.2.3-alpha.beta+exp.sha.5114f85 _fx big 10 20 30 None None 10.20.30 _fx pre-hyphen 1 0 0 "'x.7.z.92'" None 1.0.0-x.7.z.92 _fx build-hyphen 1 0 0 None "'21AF26D3---117B344092BD'" 1.0.0+21AF26D3---117B344092BD _fx rc 1 0 0 "'rc.1'" "'build.1'" 1.0.0-rc.1+build.1 # THE FIELD TYPES ARE PART OF THE CONTRACT. An absent prerelease is `None', NOT # the empty string -- clause 9 forbids an empty identifier, so `''` is a value # the grammar cannot produce. A caller writing `if v.build:` cannot tell the two # apart, which is why the type is asserted beside every value above. check_eq $P/type-major "int" "$(pv type_major)" check_eq $P/type-pre-absent "NoneType" "$(pv type_pre_absent)" check_eq $P/type-pre-present "str" "$(pv type_pre_present)" check_eq $P/type-build-present "str" "$(pv type_build_present)" # CLAUSE 2 PUTS NO BOUND ON THE NUMBERS, and Python ints are arbitrary # precision, so a version past any C implementation's range parses and renders. check_eq $P/huge-major "99999999999999999999" "$(pv huge_major)" check_eq $P/huge-rt "99999999999999999999.0.0" "$(pv huge_rt)" # `optional_minor_and_patch' IS SEMVER'S EXTENSION, NOT THE SPEC'S: clause 2 # requires all three components, so `1.2' is not a semantic version. The flag # accepts it and zero-fills; with the flag absent it is still refused, which is # the case that keeps the extension from being a loosening. check_eq $P/optional-mp "1.2.0" "$(pv optional_mp)" check_eq $P/optional-m "1.0.0" "$(pv optional_m)" check_eq $P/optional-off-refused "yes" "$(pv optional_off_refused)" # -------------------------------------------------------------- the refusals --- # THE GRAMMAR'S NOs -- the half of a specification nothing exercises by # accident. `ValueError' is the whole failure contract: semver raises it # directly and defines `is_valid' as `parse' with an except round it, so a # caller writing `except ValueError' catches everything and there is no # library-specific class to miss. The opposite shape from isodate's # ISO8601Error, and stated as a fact rather than as an absence. for _n in missing-patch missing-minor leading-zero leading-zero-mn \ leading-zero-pt pre-leading-zero empty-pre empty-pre-dot \ empty-build negative four-parts v-prefix trailing-space \ leading-space empty words plus-only pre-bad-char; do check_eq $P/bad-$_n "yes" "$(pv bad_$(echo "$_n" | tr '-' '_'))" done # THE PARSER IS ANCHORED AT BOTH ENDS. `trailing-space' and `four-parts' above # are that assertion by example; this states it positively, off the compiled # pattern, so the reason is on the record rather than inferred from a list. check_eq $P/regex-anchored-end "yes" "$(pv regex_anchored_end)" check_eq $P/valid-good "yes" "$(pv valid_good)" check_eq $P/valid-bad "no" "$(pv valid_bad)" check_eq $P/valid-empty "no" "$(pv valid_empty)" # `is_valid' TAKES A STRING and raises TypeError given a Version -- so # "validate whatever I have" does not answer False, it fails. Pinned because it # is the natural misuse of a predicate. check_eq $P/valid-typeerror "yes" "$(pv valid_typeerror)" # -------------------------------------------------------------- precedence --- # CLAUSE 11, WHICH IS THE WHOLE REASON THIS LIBRARY EXISTS. The clause is four # rules and each gets its own block: a single sorted fixture list passes if any # two rules are wrong in compensating directions. # # 11.2 -- major, minor, patch compared NUMERICALLY, in that order. `numeric-10' # is the one that catches a text comparison: 10 > 9 although "10" < "9". for _n in major minor patch equal lt numeric-10 numeric-2; do _k=$(echo "$_n" | tr '-' '_') check_eq $P/cmp-$_n "$(pv cmp_${_k}_want)" "$(pv cmp_$_k)" done # 11.3 -- A PRERELEASE HAS LOWER PRECEDENCE THAN THE ASSOCIATED NORMAL VERSION. # Asserted in BOTH directions, because `"1.0.0-alpha" < "1.0.0"' is also true as # plain text -- the right answer for the wrong reason -- while a naive tuple # comparison sorting `None' low gives the reverse. check_eq $P/pre-lower "-1" "$(pv pre_lower)" check_eq $P/pre-lower-rev "1" "$(pv pre_lower_rev)" # 11.4 -- THE SPEC'S OWN EXAMPLE CHAIN, verbatim from clause 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 for _i in 0 1 2 3 4 5 6; do check_eq $P/chain-$_i "-1" "$(pv chain_$_i)" done # ...and the same chain SORTED from a reversed input, which is a different # assertion: the pairwise cases all pass for a comparison that is not # transitive, and `sorted' is what a caller actually reaches for. check_eq $P/chain-sorted \ "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" \ "$(pv chain_sorted)" # 11.4.1 numeric identifiers numerically -- beta.11 > beta.2, the other way # round as text. It is inside the chain; stated alone because the chain would # hide it if it were the only failure. check_eq $P/pre-numeric "1" "$(pv pre_numeric)" # 11.4.2 alphanumeric identifiers in ASCII order. check_eq $P/pre-alpha "1" "$(pv pre_alpha)" # 11.4.3 numeric identifiers ALWAYS lower than alphanumeric ones -- so # `1.0.0-999' < `1.0.0-a', which no ordinary comparison gives. check_eq $P/pre-num-lt-alpha "-1" "$(pv pre_num_lt_alpha)" check_eq $P/pre-num-lt-alpha2 "-1" "$(pv pre_num_lt_alpha2)" # 11.4.4 a larger set of fields is higher, all preceding fields being equal. check_eq $P/pre-longer "1" "$(pv pre_longer)" # CLAUSE 10 -- BUILD METADATA IS IGNORED IN PRECEDENCE ENTIRELY. Two versions # differing only in build compare EQUAL and remain distinguishable by their # fields: that pair of statements is what the clause makes, and both are # asserted because an implementation comparing build metadata gets `1.0.0+a' vs # `1.0.0+b' wrong in a way no ordering test notices. check_eq $P/build-ignored "0" "$(pv build_ignored)" check_eq $P/build-ignored-eq "yes" "$(pv build_ignored_eq)" check_eq $P/build-ignored-none "0" "$(pv build_ignored_none)" check_eq $P/build-distinct-field "'a'|'b'" "$(pv build_distinct_field)" # THE TRAP THE RULE LEAVES: two versions that compare equal RENDER DIFFERENTLY, # so a set of Versions and a set of their strings do not have the same size. # `__hash__' is `hash(to_tuple()[:4])' -- build deliberately excluded -- so equal # versions hash alike, the language's invariant holds, and the set keeps exactly # one of them. An implementation hashing all five would satisfy every ordering # case above and violate the invariant silently, which is why this is measured # rather than reasoned about. check_eq $P/build-eq-but-str-differs "yes" "$(pv build_eq_but_str_differs)" check_eq $P/build-hash-differs "no" "$(pv build_hash_differs)" check_eq $P/build-set-size "1" "$(pv build_set_size)" check_eq $P/hash-tuple-len "5" "$(pv hash_tuple_len)" # THE RICH COMPARISONS are generated from `compare' by a decorator, so they # should agree with it -- "should" being the reason to measure it. The operand # is `1.0.0-alpha' against `1.0.0', i.e. clause 11.3 seen through the operators # a caller actually writes. check_eq $P/op-lt "yes" "$(pv op_lt)" check_eq $P/op-le "yes" "$(pv op_le)" check_eq $P/op-gt "no" "$(pv op_gt)" check_eq $P/op-ge "no" "$(pv op_ge)" check_eq $P/op-eq "no" "$(pv op_eq)" check_eq $P/op-ne "yes" "$(pv op_ne)" # ...and they COERCE a string, a tuple or a dict on the right, which is semver's # convenience and not the spec's -- while refusing an int with TypeError rather # than answering a comparison the caller thought had succeeded. check_eq $P/op-vs-str "yes" "$(pv op_vs_str)" check_eq $P/op-vs-tuple "yes" "$(pv op_vs_tuple)" check_eq $P/op-vs-dict "yes" "$(pv op_vs_dict)" check_eq $P/op-vs-int "yes" "$(pv op_vs_int)" # `max'/`min' over Versions follow from the operators for free -- and "give me # the newest of these" is the single commonest use of the whole package. check_eq $P/max-of "1.0.0" "$(pv max_of)" check_eq $P/min-of "1.0.0-alpha" "$(pv min_of)" # ------------------------------------------------------------------- bumps --- # CLAUSES 6, 7 AND 8. Each bump is asserted for the component it CHANGES *and* # for the ones it must RESET -- the reset is the half that is easy to get wrong # and invisible to a test that checks only the changed field: a bump_minor that # left the patch alone gives `1.2.4' where `1.3.0' was meant, and every # assertion about the minor still passes. The prerelease and build are dropped # by all three, which is the same reset seen from clauses 9 and 10. _bmp() { # name string major|minor|patch|pre|build check_eq $P/bump-$1 "$2" "$(pv bump_$(echo "$1" | tr '-' '_'))" check_eq $P/bump-$1-fields "$3" "$(pv bump_$(echo "$1" | tr '-' '_')_fields)" } _bmp major 2.0.0 "2|0|0|None|None" _bmp minor 1.3.0 "1|3|0|None|None" _bmp patch 1.2.4 "1|2|4|None|None" _bmp major-pre 2.0.0 "2|0|0|None|None" _bmp minor-pre 1.3.0 "1|3|0|None|None" _bmp patch-pre 1.2.4 "1|2|4|None|None" _bmp major-zero 1.0.0 "1|0|0|None|None" # `bump_prerelease' IS SEMVER'S CHOICE, NOT THE SPEC'S -- clause 9 says nothing # about advancing a prerelease. What 3.0.2 does is not "increment a trailing # counter": it builds `.0' when there is none and then increments the # LAST RUN OF DIGITS ANYWHERE in the string, which is why `1.0.0-x.7.z.92' goes # to `x.7.z.93' where a trailing-field rule would have to invent a field. # # `bumppre-noop-alpha' IS FINDING 2. A prerelease with no digits has nothing to # increment, so the bump returns the version UNCHANGED and does not raise. This # case is the whole record of it; a release that fixed it reds here. check_eq $P/bumppre-none "1.2.3-rc.1" "$(pv bumppre_none)" check_eq $P/bumppre-rc "1.2.3-rc.2" "$(pv bumppre_rc)" check_eq $P/bumppre-rc9 "1.2.3-rc.10" "$(pv bumppre_rc9)" check_eq $P/bumppre-noop-alpha "1.2.3-alpha" "$(pv bumppre_noop_alpha)" check_eq $P/bumppre-last-digits "1.0.0-x.7.z.93" "$(pv bumppre_last_digits)" check_eq $P/bumppre-token "1.2.3-alpha.1" "$(pv bumppre_token)" check_eq $P/bumppre-token-on "1.2.3-alpha.2" "$(pv bumppre_token_on)" # ...and it DROPS the build metadata, like every other bump. check_eq $P/bumppre-drops-build "1.2.3-rc.2" "$(pv bumppre_drops_build)" # THE THREE SPELLINGS OF "NO TOKEN" GIVE TWO ANSWERS: the default "rc" and an # explicit None both land on `rc.1'; the EMPTY STRING takes a third branch and # gives `1.2.3-1'. Exactly the shape a caller passing a variable trips over. check_eq $P/bumppre-explicit-none "1.2.3-rc.1" "$(pv bumppre_explicit_none)" check_eq $P/bumppre-empty-token "1.2.3-1" "$(pv bumppre_empty_token)" # `bump_build' HAS THE SAME SHAPE on the field clause 10 says is ignored -- so a # bumped build compares EQUAL to the version it came from. That is not a defect; # it is clause 10 working, and it is asserted beside the bump so the point is on # the record rather than discovered later. check_eq $P/bumpbuild-none "1.2.3+build.1" "$(pv bumpbuild_none)" check_eq $P/bumpbuild-build "1.2.3+build.2" "$(pv bumpbuild_build)" check_eq $P/bumpbuild-named "1.2.3+b.10" "$(pv bumpbuild_named)" check_eq $P/bumpbuild-token "1.2.3+sha.1" "$(pv bumpbuild_token)" # ...and unlike every other bump it KEEPS the prerelease, because build and # prerelease are independent fields rather than a hierarchy. check_eq $P/bumpbuild-on-pre "1.2.3-rc.1+build.1" "$(pv bumpbuild_on_pre)" check_eq $P/bumpbuild-compares-equal "0" "$(pv bumpbuild_compares_equal)" check_eq $P/bump-is-new-object "yes" "$(pv bump_is_new_object)" check_eq $P/bump-leaves-original "1.2.3" "$(pv bump_leaves_original)" # `next_version' IS THE ONE THAT KNOWS ABOUT PRERELEASES, and its whole point is # that it is NOT bump_: `next_version("patch")' on `1.2.3-rc.1' is # `1.2.3' -- the release the prerelease was leading to -- where `bump_patch' # would give `1.2.4' and skip it. Semver's own, not the spec's. check_eq $P/next-major-plain "'2.0.0'" "$(pv next_major_plain)" check_eq $P/next-minor-plain "'1.3.0'" "$(pv next_minor_plain)" check_eq $P/next-patch-plain "'1.2.4'" "$(pv next_patch_plain)" check_eq $P/next-patch-on-pre "'1.2.3'" "$(pv next_patch_on_pre)" check_eq $P/next-minor-on-pre "'1.3.0'" "$(pv next_minor_on_pre)" check_eq $P/next-major-on-pre "'2.0.0'" "$(pv next_major_on_pre)" check_eq $P/next-pre-on-plain "'1.2.4-rc.1'" "$(pv next_pre_on_plain)" check_eq $P/next-pre-on-pre "'1.2.3-rc.2'" "$(pv next_pre_on_pre)" check_eq $P/next-bad-part "yes" "$(pv next_bad_part)" # `build' IS REFUSED BY NAME, and rightly: "the next build" is not a question # the spec's ordering can answer, since clause 10 excludes build from it. check_eq $P/next-build-refused "yes" "$(pv next_build_refused)" check_eq $P/finalize-pre "1.2.3" "$(pv finalize_pre)" check_eq $P/finalize-both "1.2.3" "$(pv finalize_both)" check_eq $P/finalize-plain "1.2.3" "$(pv finalize_plain)" # ------------------------------------------------------------------- match --- # THE SIX COMPARATOR EXPRESSIONS the docstring documents, plus the bare-version # form that means `=='. `match' is what a caller reaches for "does this version # satisfy X", so its REFUSALS matter as much as its hits. for _c in ge-true ge-eq ge-false gt-false gt-true lt-true le-eq eq-true \ ne-true ne-false bare bare-false pre-vs-rel build-ign; do _k=$(echo "$_c" | tr '-' '_') case "$_c" in ge-false|gt-false|ne-false|bare-false) _want=no ;; *) _want=yes ;; esac check_eq $P/match-$_c "$_want" "$(pv match_$_k)" done # NOT A RANGE SYNTAX, AND IT SAYS SO LOUDLY. `^1.0.0' and `~1.0.0' are npm's, # not the spec's -- the spec defines no ranges at all -- and semver RAISES on # them rather than quietly answering False, which is what a caller filtering a # list would never notice. for _c in caret tilde v empty word; do check_eq $P/match-bad-$_c "yes" "$(pv match_bad_$_c)" done # An expression whose OPERATOR is fine but whose VERSION is not raises from the # parse underneath -- same exception class, different cause. A caller cannot # tell "bad operator" from "bad version", and the two need different fixes. check_eq $P/match-bad-version "yes" "$(pv match_bad_version)" # ------------------------------------------------- the rest of the surface --- # CONVERSIONS. `to_dict'/`to_tuple' are how a Version leaves the type, and both # carry all five fields INCLUDING the Nones -- a dict that dropped absent keys # would round-trip through `Version(**d)' today and lose a field tomorrow. check_eq $P/to-tuple "(1, 2, 3, 'rc.1', 'b.2')" "$(pv to_tuple)" check_eq $P/to-dict-keys "['build', 'major', 'minor', 'patch', 'prerelease']" "$(pv to_dict_keys)" check_eq $P/to-dict-absent "None" "$(pv to_dict_absent)" check_eq $P/from-dict-rt "1.2.3-rc.1+b.2" "$(pv from_dict_rt)" check_eq $P/from-tuple-rt "1.2.3-rc.1+b.2" "$(pv from_tuple_rt)" check_eq $P/iter "(1, 2, 3, 'rc.1', 'b.2')" "$(pv iter)" # INDEXING. `v[0]' is the major and `v[:4]' the form `is_compatible' uses # internally, so the slice is not a curiosity. A NEGATIVE index and an # out-of-range one raise IndexError; a STEP does not raise at all -- it strides # the five fields like any tuple slice, which the docstring's talk of "the first # N parts" reads as though it checked. check_eq $P/index-0 "1" "$(pv index_0)" check_eq $P/index-slice "(1, 2, 3)" "$(pv index_slice)" check_eq $P/index-slice4 "(1, 2, 3, 'rc.1')" "$(pv index_slice4)" check_eq $P/index-step "(1, 3)" "$(pv index_step)" check_eq $P/index-negative "yes" "$(pv index_negative)" check_eq $P/index-oob "yes" "$(pv index_oob)" check_eq $P/replace-major "9.2.3" "$(pv replace_major)" check_eq $P/replace-pre "1.2.3-rc.1" "$(pv replace_pre)" check_eq $P/replace-drop "1.2.3" "$(pv replace_drop)" check_eq $P/replace-unknown "yes" "$(pv replace_unknown)" # IMMUTABILITY. `__slots__' means no attribute can be added, and every property # setter raises AttributeError -- so 2.x code doing `v.major = 2' fails loudly # instead of mutating a value other references share. for _n in major minor patch prerelease build new-attr; do check_eq $P/immutable-$_n "yes" "$(pv immutable_$(echo "$_n" | tr '-' '_'))" done # FINDING 1 -- THE CONSTRUCTOR DOES NOT VALIDATE. `Version.parse' refuses # eighteen shapes above and NONE of that is reached by `Version(...)', which # checks only that the numbers are not negative. So: # - `ctor-bad-pre-refused' is `no' -- it accepts `a_b', which clause 9 bars. # - `ctor-bad-pre-renders' is the invalid string it produced. # - `ctor-roundtrip-broken' is `yes' -- `parse' will not read it back. # - `ctor-float-refused' is `no' -- a float is truncated through `int()'. # - `replace-bad-pre' is the same defect through the documented setter. # `ctor-negative' is the control that says the constructor checks SOMETHING. # Upstream's, in a third-party package; pinned rather than reported, and a # release that fixes it reds on these five and says so. check_eq $P/ctor-negative "yes" "$(pv ctor_negative)" check_eq $P/ctor-float-refused "no" "$(pv ctor_float_refused)" check_eq $P/ctor-float-truncates "1.0.0" "$(pv ctor_float_truncates)" check_eq $P/ctor-bad-pre-refused "no" "$(pv ctor_bad_pre_refused)" check_eq $P/ctor-bad-pre-renders "1.0.0-a_b" "$(pv ctor_bad_pre_renders)" check_eq $P/ctor-roundtrip-broken "yes" "$(pv ctor_roundtrip_broken)" check_eq $P/replace-bad-pre "1.2.3-a_b" "$(pv replace_bad_pre)" check_eq $P/ctor-defaults "1.0.0" "$(pv ctor_defaults)" check_eq $P/ctor-int-pre "1.0.0-1" "$(pv ctor_int_pre)" # `is_compatible' IS SEMVER'S OWN, and it is DIRECTIONAL -- `a.is_compatible(b)' # asks whether b may replace a. The two directions differ, so a caller who wrote # it the other way round gets a wrong answer with no error. Its docstring is # explicit that it does NOT check patches and that major-0 is incompatible with # everything but itself, which is clause 4's "anything MAY change at any time" # made executable. check_eq $P/compat-newer-minor "yes" "$(pv compat_newer_minor)" check_eq $P/compat-older-minor "no" "$(pv compat_older_minor)" check_eq $P/compat-same "yes" "$(pv compat_same)" check_eq $P/compat-patch-ign "yes" "$(pv compat_patch_ign)" check_eq $P/compat-major-diff "no" "$(pv compat_major_diff)" check_eq $P/compat-zero-same "yes" "$(pv compat_zero_same)" check_eq $P/compat-zero-minor "no" "$(pv compat_zero_minor)" check_eq $P/compat-pre-differs "no" "$(pv compat_pre_differs)" check_eq $P/compat-pre-same "yes" "$(pv compat_pre_same)" # ...and it REFUSES a string, unlike every comparison operator above, which # coerces one. Same package, two conventions; the inconsistency is what a caller # trips over, so it is pinned rather than smoothed over. check_eq $P/compat-str-refused "yes" "$(pv compat_str_refused)" # -------------------------------------------------- the deprecated 2.x API --- # TWENTY-ONE MODULE-LEVEL FUNCTIONS, ALL DEPRECATED, ALL STILL CORRECT. Two # separate assertions: that they give the RIGHT ANSWER (a caller on the old API # is not silently broken) and that they WARN (so the deprecation is real rather # than documentation). The warning is the half that would go unnoticed -- # DeprecationWarning is suppressed by default outside __main__, so nothing # prints it in ordinary use and only an explicit `catch_warnings' sees it. check_eq $P/dep-parse "[('build', 'b.2'), ('major', 1), ('minor', 2), ('patch', 3), ('prerelease', 'rc.1')]" "$(pv dep_parse)" check_eq $P/dep-compare "-1" "$(pv dep_compare)" check_eq $P/dep-match "yes" "$(pv dep_match)" check_eq $P/dep-bump-major "2.0.0" "$(pv dep_bump_major)" check_eq $P/dep-bump-minor "1.3.0" "$(pv dep_bump_minor)" check_eq $P/dep-bump-patch "1.2.4" "$(pv dep_bump_patch)" check_eq $P/dep-bump-pre "1.2.3-rc.1" "$(pv dep_bump_pre)" check_eq $P/dep-bump-build "1.2.3+build.1" "$(pv dep_bump_build)" check_eq $P/dep-finalize "1.2.3" "$(pv dep_finalize)" check_eq $P/dep-format "1.2.3-rc.1+b.2" "$(pv dep_format)" check_eq $P/dep-max "2.0.0" "$(pv dep_max)" check_eq $P/dep-min "1.0.0" "$(pv dep_min)" check_eq $P/dep-replace "9.2.3" "$(pv dep_replace)" # `parse_version_info' RETURNS A Version, not a namedtuple -- the 2.x name for # the 3.x class, which is what `VersionInfo' being an alias makes possible. check_eq $P/dep-parse-version-info "'Version'" "$(pv dep_parse_version_info)" # FINDING 3 -- THE CATEGORY IS NOT UNIFORM, and it is a decision rather than an # inconsistency: `compare' and `match' carry PendingDeprecationWarning because # whether the module-level comparison API survives at all is still open upstream # (its issue #258, quoted in the warning's own text), while `parse' and the # bumps carry a plain DeprecationWarning. It matters to a caller: # `-W error::DeprecationWarning' turns one set into failures and leaves the # other running. check_eq $P/dep-warns-parse "DeprecationWarning" "$(pv dep_warns_parse)" check_eq $P/dep-warns-compare "PendingDeprecationWarning" "$(pv dep_warns_compare)" check_eq $P/dep-warns-match "DeprecationWarning" "$(pv dep_warns_match)" check_eq $P/dep-warns-bump-major "DeprecationWarning" "$(pv dep_warns_bump_major)" check_eq $P/dep-warns-format-version "DeprecationWarning" "$(pv dep_warns_format_version)" check_eq $P/dep-warns-max-ver "DeprecationWarning" "$(pv dep_warns_max_ver)" # The warning NAMES ITS REPLACEMENT, which is what makes a deprecation # actionable rather than a nag. Asserted once per category. check_eq $P/dep-warn-names-replacement "yes" "$(pv dep_warn_names_replacement)" check_eq $P/dep-warn-names-replacement-parse "yes" "$(pv dep_warn_names_replacement_parse)" # ...and the CLASS methods do NOT warn. A suite that only checked "something # warns" would pass with everything warning, which is the wrong library. check_eq $P/dep-class-quiet "yes" "$(pv dep_class_quiet)" # --------------------------------------------------------- the cli, in-process --- # The same code path the shipped program reaches, exercised inside the probe's # own interpreter start. It is cheap, and it is what says whether a failure in # the PROCESS cases below is the program or the plumbing round it. check_eq $P/cli-bump-major "0|2.0.0" "$(pv cli_bump_major)" check_eq $P/cli-bump-minor "0|1.3.0" "$(pv cli_bump_minor)" check_eq $P/cli-compare-lt "0|-1" "$(pv cli_compare_lt)" check_eq $P/cli-compare-eq "0|0" "$(pv cli_compare_eq)" check_eq $P/cli-check-good "0|" "$(pv cli_check_good)" check_eq $P/cli-nextver "0|1.3.0" "$(pv cli_nextver)" # THE FAILURE PATH IS AN EXIT STATUS: an invalid version makes `check' return 2 # with its message on stderr, which is what a shell script guarding on # `pysemver check "$v"' relies on. check_eq $P/cli-check-bad "2|" "$(pv cli_check_bad)" check_eq $P/cli-no-args-head "exit:None|usage: semver [-h] [--version]" "$(pv cli_no_args_head)" # ------------------------------------------------------ the cli, as a process --- # THE ONLY WAY TO MEASURE A SHEBANG AND AN EXIT STATUS. `/usr/bin/pysemver' is # generated by the recipe from the console_scripts entry point and starts # `#!/usr/bin/env python3', so running it exercises three things the in-process # cases cannot: that `env' resolves python3 on this image's PATH, that # site-packages is importable from a program started outside the interpreter's # own directory, and that the process exit status matches the return value. # # This is a FOURTH interpreter start, and it is the reason the timeout above is # 1500 rather than 1200. check_out $P/script-bump "2.0.0" /usr/bin/pysemver bump major 1.2.3 check_out $P/script-compare "-1" /usr/bin/pysemver compare 1.0.0 2.0.0 check_rc $P/script-check-good 0 /usr/bin/pysemver check 1.2.3 check_rc $P/script-check-bad 2 /usr/bin/pysemver check 1.2 # ...and the shebang is `env python3', which is a statement about the RECIPE's # script generation rather than about semver: a recipe that hard-coded a build # host's interpreter path would ship a program that cannot run here at all. check_out $P/script-shebang "#!/usr/bin/env python3" head -n 1 /usr/bin/pysemver # ------------------------------------------------ upstream's own fixture data --- # THE AUTHOR'S TABLES, run here because the runner that reads them is not on # this image (see the ptest note at the head of this file). 31 strings upstream # says must parse and 35 it says must not -- and the round trip over the whole # valid table, which is one assertion covering all 31: every one must render # back to itself, since clause 2 permits no normalisation. # # Each of the three cases below reports the OFFENDING STRINGS rather than a # count, so a red names what diverged instead of saying how many did. check_eq $P/upstream-valid-count "31" "$(pv upstream_valid_count)" check_eq $P/upstream-valid-rejected "" "$(pv upstream_valid_rejected)" check_eq $P/upstream-valid-rt-broken "" "$(pv upstream_valid_rt_broken)" check_eq $P/upstream-invalid-count "35" "$(pv upstream_invalid_count)" check_eq $P/upstream-invalid-accepted "" "$(pv upstream_invalid_accepted)" rm -rf "$W"