. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts and nothing else. # # # # They are: a bare `import', a second one whose STDERR is # # the assertion (`import-quiet'), and the probe, which # # takes ONE start for 150 measurements. # # # # 400s per start is the budget the neighbouring python3 # # suites use on this arm. NOTHING here forks, execs, opens # # a file or touches a clock: every case is a string parsed # # in memory, so the interpreter start IS the whole cost # # and there is no other traffic to budget for. # # # # A suite-timeout red is a batch-wide cost, so if the # # 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-awesomeversion 24.2.0 (meta-openembedded meta-python/recipes-devtools/ # python, python3-awesomeversion_24.2.0.bb, `inherit pypi python_poetry_core', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # `awesomeversion' takes a version STRING, decides which versioning scheme it # follows, and compares two of them against each other EVEN WHEN THE TWO FOLLOW # DIFFERENT SCHEMES. That last clause is the library's reason to exist and the # thing no stdlib module does. # # https://github.com/ludeeus/awesomeversion # # So it is pure string work: no socket, no subprocess, no clock, no filesystem, # no device and no terminal. Every case below is a literal in and a literal # out. That is what makes this port close on its suite with no BRINTOS-780 # follow-on -- and `no-socket-module' / `no-subprocess-module' at the bottom # assert the claim rather than leaving it as an assumption. # # ============================================================================ # STRATEGY IS DECIDED BY FIRST MATCH IN A FIXED ORDER, NOT BY BEST FIT. # ============================================================================ # # This is the single most surprising thing about the library, and most of the # `strategy-*' block exists to pin it. `AwesomeVersion.strategy' walks # `VERSION_STRATEGIES' -- a TUPLE, in this order -- # # BuildVer, CalVer, HexVer, SemVer, SpecialContainer, SimpleVer, PEP 440 # # and returns the FIRST pattern that matches the whole string. It does not # score the candidates and it does not prefer the most specific. So: # # `1' is BuildVer (`\d+' matches) and NOT PEP 440, which also would. # `24.2.0' is CalVer (RE_CALVER's leading `\d{2}') and NOT SemVer, # which also would -- and `24.2.0' is THE PACKAGE'S # OWN VERSION, so the library classifies itself as # CalVer. # `1.2.3' is SemVer only because RE_CALVER needs two or four leading # digits and `1' is one. # # Every fixture in the classification block is therefore chosen so that exactly # ONE pattern can claim it, which is what makes each case name a strategy # rather than an accident of position. The three `firstmatch-*' cases then # state the rule itself, against strings where the earlier pattern DOES win. # # ============================================================================ # ensure_strategy DOES NOT VALIDATE. IT REORDERS. THIS IS THE TRAP. # ============================================================================ # # A caller reading the keyword's name will use it as a guard: "raise unless # this version is SemVer". It is not that. `strategy' walks the ensure list # FIRST and the fixed tuple afterwards, so naming a strategy MOVES IT TO THE # FRONT of the match order and can CHANGE the answer instead of asserting it: # # AwesomeVersion('2024.1.0').strategy -> CalVer # AwesomeVersion('2024.1.0', ensure_strategy=SEMVER).strategy -> SemVer # # No exception is raised, because `2024.1.0' matches the SemVer pattern too -- # the ensure list simply put SemVer ahead of CalVer. It raises ONLY when # nothing in the list matches at all (`ensure-real-mismatch', # `ensure-calver-on-semver'). A caller who wrote `ensure_strategy=' expecting a # guard has silently RECLASSIFIED the version, and every `.strategy' test # downstream now answers differently. # # The four cases state both halves side by side, so the distinction is on the # record rather than left to whichever one a reader happens to try first. # # ============================================================================ # THREE MORE THINGS A CALLER WILL GET WRONG. # ============================================================================ # # `version == None' RAISES. It does not return False. `__eq__' coerces a # str/int/float and raises AwesomeVersionCompareException for # anything else, so the single most common null check in # Python throws on this type -- and `!=' throws with it, # since `__ne__' is `not __eq__'. Both are asserted. An # `except' wrapped around a `<' comparison does not cover it, # because nobody expects an equality test to be the thing # that fails. # # `==' AND `<' DISAGREE ABOUT AN UNPARSEABLE VERSION. Comparing an UNKNOWN # with `<' or `>' raises AwesomeVersionCompareException, as # the ticket requires. But `==' on two UNKNOWNs does NOT # raise -- it falls through to a plain string comparison and # returns True or False. So an unparseable version is usable # for equality and unusable for ordering, which is not a # distinction the API surfaces anywhere. # # `str()' AND `repr()' DISAGREE ABOUT THE PREFIX. `str(AwesomeVersion( # 'v1.2.3'))' is `v1.2.3'; its `repr' is # `' -- prefix stripped. The # `.string' property is the stripped form and is what `hash' # and `==' use, which is why `v1.0.0' and `1.0.0' are one # dict key. Asserted as a group, because a reader who checks # one of the three will assume the other two. # # ============================================================================ # THE RECIPE DECLARES TWO RUNTIME DEPENDENCIES THE PACKAGE NEVER IMPORTS. # ============================================================================ # # meta-python's recipe carries `RDEPENDS:${PN} += "python3-profile # python3-logging"'. The package's complete set of non-relative imports, across # all 13 files, is `__future__', `collections.abc', `dataclasses', `enum', # `re', `typing' and `warnings' -- every one of them in poky's `core' split. # There is no `import logging' and no `import profile' anywhere in the tree. # # python3-awesomeversion_%.bbappend removes both, and records the refusal # measurement that justifies it: this probe's 150 measurements were re-run on # an x86-64 control with `logging' and `profile' refused at `sys.meta_path', # and the output was BYTE-IDENTICAL. `no-logging-module' below is the # on-target half of that: after every case has run, `logging' is still absent # from `sys.modules'. # # ============================================================================ # HOW THE SUITE IS SHAPED. # ============================================================================ # # Three layers. THE TREE -- the files the .deb actually installed, the # dist-info, and the dpkg control stanza. THE IMPORT -- that it works, and that # it is silent. THE BEHAVIOUR -- 150 measurements taken in ONE python3 start by # data/python3-awesomeversion-probe.py, which emits `key=value' lines this file # reads back with grep. # # The one start is not a style choice: a python3 start on this arm is tens of # seconds, so a start per measurement would be the difference between a # one-minute suite and a half-hour one, on an image every other member of this # batch is waiting to verify against. # # EVERY EXPECTED VALUE BELOW WAS MEASURED, NOT PREDICTED. They were taken from # this same probe run against the sdist the recipe's SRC_URI sha256 pins, on an # x86-64 CPython 3.12 control -- the same minor this image ships. Two of them # contradicted what reading the source suggested (`ensure-reorders-strategy' # and `eq-vs-none'), which is the reason the rule is measure-then-assert rather # than assert-then-measure. P=python3-awesomeversion # The suite reported at all -- the case that separates "everything passed" from # "this file never ran". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/awesomeversion DI=$SITE/awesomeversion-24.2.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-awesomeversion.list PROBE=$BST_DATA/python3-awesomeversion-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-awesomeversion-suite.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------------------- # LAYER 1 -- THE TREE. What the .deb actually put on the image. # --------------------------------------------------------------------------- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/not-a-flat-module test ! -e "$SITE/awesomeversion.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/main-module test -s "$PKGDIR/awesomeversion.py" check $P/strategy-file test -s "$PKGDIR/strategy.py" check $P/exceptions-file test -s "$PKGDIR/exceptions.py" check $P/typing-file test -s "$PKGDIR/typing.py" check $P/utils-subpackage test -e "$PKGDIR/utils/__init__.py" check $P/utils-regex test -s "$PKGDIR/utils/regex.py" check $P/utils-validate test -s "$PKGDIR/utils/validate.py" check $P/handlers-subpkg test -e "$PKGDIR/comparehandlers/__init__.py" check $P/handlers-container test -s "$PKGDIR/comparehandlers/container.py" check $P/handlers-modifier test -s "$PKGDIR/comparehandlers/modifier.py" check $P/handlers-sections test -s "$PKGDIR/comparehandlers/sections.py" check $P/handlers-simple test -s "$PKGDIR/comparehandlers/simple.py" # The package ships a PEP 561 marker, which is the whole reason its `typing.py' # and the `py.typed' file are in the runtime package rather than a -dev split. check $P/py-typed-marker test -f "$PKGDIR/py.typed" # Byte-compiled on the image, not shipped as source only. check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION. This image's python3 is one static binary with an # empty lib-dynload and no dynamic loader (BRINTOS-1476), so a release that # grew a .so would install it and then die at import. Asserted directly, so a # version bump that grows one fails HERE rather than as an ImportError three # tickets later. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # A library, not a tool: it installs no executable. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(s?bin)/' "$W/pkgf" 2>/dev/null)" # Upstream's own tests are NOT in the distribution -- pyproject.toml's # `include' names only `awesomeversion' and `awesomeversion.*', so the repo's # tests/ directory never reaches site-packages. Asserted rather than assumed, # because the neighbouring python3-aniso8601 DOES ship its unittest files into # the runtime .deb, and the difference is one line of packaging metadata. check_eq $P/no-tests-package "" \ "$(grep -E "^$SITE/tests(/|\$)" "$W/pkgf" 2>/dev/null)" # Everything the package installs is Python or Python metadata. `py.typed' and # the stray top-level LICENCE.md are the two non-.py files it is allowed. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */py.typed) continue ;; "$SITE/LICENCE.md") continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE LICENCE LANDS IN site-packages ITSELF, not under the dist-info. That is # poetry's `include = [..., "LICENCE.md"]' resolving relative to the wheel # root, so the file is installed as `site-packages/LICENCE.md' where it is # shared namespace with every other package on the image. It is a packaging # wart, not a defect in this port, and it is asserted so that the day upstream # moves it the change is visible rather than silent. check $P/licence-in-site-packages test -s "$SITE/LICENCE.md" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: awesomeversion" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 24.2.0" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # PURE PYTHON, ARCH-INDEPENDENT -- the wheel metadata's own statement of the # fact `pkg-no-so' asserts structurally. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-generator "poetry-core" cat "$DI/WHEEL" check_eq $P/dist-info-no-arch-tag "" \ "$(grep '^Tag: ' "$DI/WHEEL" 2>/dev/null | grep -v '^Tag: py3-none-any$')" # poetry writes no `top_level.txt' -- setuptools does. Asserted as an ABSENCE # so that the neighbouring setuptools ports' `dist-info-top-level' case is not # copied in here where it would fail for a reason that is not a defect. check $P/dist-info-no-top-level test ! -e "$DI/top_level.txt" sed -n '/^Package: python3-awesomeversion$/,/^$/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: 24.2.0-r0" cat "$W/st" # THE bbappend TOOK. python3-awesomeversion_%.bbappend removes the two RDEPENDS # meta-python declares and the package never imports; this is the assertion # that the removal reached the built package rather than merely the recipe. # A red here means the bbappend stopped applying -- a rename, a version bump # past the `%' glob -- and the image silently grew both splits back. check_eq $P/rdepends-no-logging "" \ "$(grep '^Depends:' "$W/st" 2>/dev/null | grep -o 'python3-logging')" check_eq $P/rdepends-no-profile "" \ "$(grep '^Depends:' "$W/st" 2>/dev/null | grep -o 'python3-profile')" check_out $P/rdepends-keeps-core "python3-core" grep '^Depends:' "$W/st" # --------------------------------------------------------------------------- # LAYER 2 -- THE IMPORT. That it works, and that it is SILENT. # --------------------------------------------------------------------------- check $P/import python3 -c 'import awesomeversion' # A clean import writes NOTHING. The package's only import-time work is # compiling seven regexes and building an Enum, so a diagnostic here means a # DeprecationWarning from the interpreter or a broken install -- and either # would corrupt the probe's output, which this suite parses line by line. check_eq $P/import-quiet "" \ "$(python3 -c 'import awesomeversion' 2>&1)" # --------------------------------------------------------------------------- # LAYER 3 -- THE BEHAVIOUR. 150 measurements, ONE python3 start. # --------------------------------------------------------------------------- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # The probe writes nothing to stderr. Asserted, because the library's only # stderr path is `warnings', and a warning appearing where this file expects # none would mean the deprecation cases below are measuring something else. check_eq $P/probe-stderr-clean "" "$(cat "$OUT".err 2>/dev/null)" # The probe reached its last line, and emitted exactly as many values as it # meant to. Together these separate "the library answered wrongly" from "the # probe died a third of the way in", which every case below would otherwise # report as an empty-string mismatch. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "150" "$(pv emitted_count)" # The module was imported from the package this suite has been asserting about, # and not from a stray copy earlier on sys.path. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # --------------------------------------------------------------------------- # LAYER 3a -- THE MODULE ITSELF. Where it was imported from, and what it # exports. A wheel that installed a flat module, or a release that dropped a # name from `__all__', fails here rather than as an AttributeError buried in a # later case. # --------------------------------------------------------------------------- check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/all-exports "AwesomeVersion,AwesomeVersionCompareException,AwesomeVersionDiff,AwesomeVersionException,AwesomeVersionStrategy,AwesomeVersionStrategyException,COMPARABLE_STRATEGIES" "$(pv all_exports)" check_eq $P/strategy-enum-members "BUILDVER,CALVER,HEXVER,SEMVER,SIMPLEVER,PEP440,UNKNOWN,SPECIALCONTAINER" "$(pv strategy_enum_members)" check_eq $P/strategy-enum-values "BuildVer,CalVer,HexVer,SemVer,SimpleVer,PEP 440,unknown,SpecialContainer" "$(pv strategy_enum_values)" check_eq $P/comparable-strategies "BuildVer,CalVer,HexVer,SemVer,SimpleVer,PEP 440" "$(pv comparable_strategies)" check_eq $P/strategy-is-str "yes" "$(pv strategy_is_str)" check_eq $P/strategy-str-eq "yes" "$(pv strategy_str_eq)" check_eq $P/exc-base-is-exception "yes" "$(pv exc_base_is_exception)" check_eq $P/exc-compare-is-base "yes" "$(pv exc_compare_is_base)" check_eq $P/exc-strategy-is-base "yes" "$(pv exc_strategy_is_base)" check_eq $P/av-is-str-subclass "yes" "$(pv av_is_str_subclass)" # --------------------------------------------------------------------------- # LAYER 3b -- STRATEGY CLASSIFICATION, one case per strategy the package # documents, against a string no EARLIER pattern in the tuple can claim. # # Order matters and is asserted separately below: the library returns the FIRST # pattern that matches, walking BuildVer, CalVer, HexVer, SemVer, # SpecialContainer, SimpleVer, PEP 440. Each fixture here is chosen so that # exactly one of those can claim it, which is what makes the case name a # strategy rather than a position. # --------------------------------------------------------------------------- check_eq $P/strategy-buildver "BuildVer" "$(pv strategy_buildver)" check_eq $P/strategy-calver "CalVer" "$(pv strategy_calver)" check_eq $P/strategy-hexver "HexVer" "$(pv strategy_hexver)" check_eq $P/strategy-semver "SemVer" "$(pv strategy_semver)" check_eq $P/strategy-simplever "SimpleVer" "$(pv strategy_simplever)" check_eq $P/strategy-pep440 "PEP 440" "$(pv strategy_pep440)" check_eq $P/strategy-container-latest "SpecialContainer" "$(pv strategy_container_latest)" check_eq $P/strategy-container-stable "SpecialContainer" "$(pv strategy_container_stable)" check_eq $P/strategy-container-beta "SpecialContainer" "$(pv strategy_container_beta)" check_eq $P/strategy-container-dev "SpecialContainer" "$(pv strategy_container_dev)" check_eq $P/strategy-unknown "unknown" "$(pv strategy_unknown)" check_eq $P/strategy-empty "unknown" "$(pv strategy_empty)" # THE FIRST-MATCH RULE, STATED DIRECTLY. Each of these is claimed by an EARLIER # pattern than a reader would predict, and together they are the reason the # cases above had to be chosen so carefully. `24.2.0' is the package's OWN # version, and it is CalVer, not SemVer. check_eq $P/firstmatch-own-version "CalVer" "$(pv firstmatch_own_version)" check_eq $P/firstmatch-plain-digit "BuildVer" "$(pv firstmatch_plain_digit)" check_eq $P/firstmatch-two-digit-year "CalVer" "$(pv firstmatch_two_digit_year)" # `valid' is exactly "not UNKNOWN" -- including for the empty string, which is # UNKNOWN and therefore invalid rather than a special case. check_eq $P/valid-semver "yes" "$(pv valid_semver)" check_eq $P/valid-garbage "no" "$(pv valid_garbage)" check_eq $P/valid-empty "no" "$(pv valid_empty)" # `simple' is a SEPARATE question from strategy: it asks only whether the string # is dotted digits with an optional `v' prefix. A BuildVer is not simple (no # dot) and a version with a modifier is not simple, which is not what the two # names suggest. check_eq $P/simple-semver "yes" "$(pv simple_semver)" check_eq $P/simple-prefixed "yes" "$(pv simple_prefixed)" check_eq $P/simple-buildver "no" "$(pv simple_buildver)" check_eq $P/simple-modifier "no" "$(pv simple_modifier)" # --------------------------------------------------------------------------- # LAYER 3c -- COMPARISON AS A TOTAL ORDERING. # # Each list is SORTED by the library and compared against the order this suite # wrote. One assertion therefore covers every pair in the list rather than N^2 # separate ones -- and sorting is exactly how a caller uses the library, so the # case exercises it the way it is actually used. # --------------------------------------------------------------------------- check_eq $P/sort-semver "1.0.0,1.0.1,1.1.0,2.0.0" "$(pv sort_semver)" check_eq $P/sort-prerelease "1.0.0-dev1,1.0.0-alpha1,1.0.0-beta1,1.0.0-rc1,1.0.0" "$(pv sort_prerelease)" check_eq $P/sort-calver "2023.12.0,2024.1.0,2024.2.0,2024.10.0" "$(pv sort_calver)" # Container tags order among THEMSELVES by the package's own map # (stable < beta < latest < dev -- note `beta' below `latest', which is not # alphabetical and not obvious), and above ANY numeric version however large. check_eq $P/sort-container "stable,beta,latest,dev" "$(pv sort_container)" check_eq $P/container-gt-numeric "yes" "$(pv container_gt_numeric)" check_eq $P/numeric-lt-container "yes" "$(pv numeric_lt_container)" # THE MIXED-STRATEGY COMPARISONS, which are the library's reason to exist: two # versions following DIFFERENT schemes still compare. check_eq $P/mixed-semver-lt-calver "yes" "$(pv mixed_semver_lt_calver)" check_eq $P/mixed-buildver-lt-semver "yes" "$(pv mixed_buildver_lt_semver)" check_eq $P/mixed-simple-vs-semver "yes" "$(pv mixed_simple_vs_semver)" # The `v' prefix is not part of the comparison: `v1.0.0' and `1.0.0' are EQUAL, # and neither is greater. This is the same fact the hashing cases depend on. check_eq $P/prefix-equal "yes" "$(pv prefix_equal)" check_eq $P/prefix-not-gt "no" "$(pv prefix_not_gt)" # Reflexivity and the strict/non-strict pairs, which `sorted' does not reach. check_eq $P/cmp-self-lt "no" "$(pv cmp_self_lt)" check_eq $P/cmp-self-gt "no" "$(pv cmp_self_gt)" check_eq $P/cmp-self-le "yes" "$(pv cmp_self_le)" check_eq $P/cmp-self-ge "yes" "$(pv cmp_self_ge)" check_eq $P/cmp-ne "yes" "$(pv cmp_ne)" # A bare str/int/float on the right-hand side is COERCED, so a caller may # compare against a literal without wrapping it. check_eq $P/cmp-vs-str "yes" "$(pv cmp_vs_str)" check_eq $P/cmp-vs-int "yes" "$(pv cmp_vs_int)" check_eq $P/cmp-vs-float "yes" "$(pv cmp_vs_float)" # `in_range' is INCLUSIVE at both ends -- asserted at both edges, because an # off-by-one here is invisible in the interior case. check_eq $P/in-range-inside "yes" "$(pv in_range_inside)" check_eq $P/in-range-low-edge "yes" "$(pv in_range_low_edge)" check_eq $P/in-range-high-edge "yes" "$(pv in_range_high_edge)" check_eq $P/in-range-outside "no" "$(pv in_range_outside)" # --------------------------------------------------------------------------- # LAYER 3d -- THE ACCESSORS, each against the literal it was parsed from. # --------------------------------------------------------------------------- check_eq $P/modifier-value "b4" "$(pv modifier_value)" check_eq $P/modifier-type "b" "$(pv modifier_type)" check_eq $P/modifier-none "None" "$(pv modifier_none)" check_eq $P/modifier-type-none "None" "$(pv modifier_type_none)" # `prefix' / `string' are a pair: `string' is the version with the prefix # REMOVED, while `str()' keeps it. Both are asserted, because the two differ # and callers reach for whichever they happened to learn first. check_eq $P/prefix-v ''"'"'v'"'"'' "$(pv prefix_v)" check_eq $P/prefix-upper-v ''"'"'V'"'"'' "$(pv prefix_upper_v)" check_eq $P/prefix-absent "None" "$(pv prefix_absent)" check_eq $P/string-strips-prefix "1.0.0" "$(pv string_strips_prefix)" check_eq $P/string-keeps-body "1.0.0" "$(pv string_keeps_body)" # The four release-phase flags, each asserted BOTH ways -- present on a version # that carries the marker, and absent on one that does not. Asserting only the # positive would pass against a property hardwired to True. check_eq $P/flag-alpha "yes" "$(pv flag_alpha)" check_eq $P/flag-beta "yes" "$(pv flag_beta)" check_eq $P/flag-dev "yes" "$(pv flag_dev)" check_eq $P/flag-rc "yes" "$(pv flag_rc)" check_eq $P/flag-alpha-absent "no" "$(pv flag_alpha_absent)" check_eq $P/flag-beta-absent "no" "$(pv flag_beta_absent)" check_eq $P/flag-dev-absent "no" "$(pv flag_dev_absent)" check_eq $P/flag-rc-absent "no" "$(pv flag_rc_absent)" # major/minor/patch return an AwesomeVersion or None, and WHICH of the two # depends on the strategy as well as on the section count: a BuildVer has a # major and no minor, and a container tag or an unknown has none at all. # `micro' aliases `patch' and `year' aliases `major'. check_eq $P/major-semver "1" "$(pv major_semver)" check_eq $P/minor-semver "2" "$(pv minor_semver)" check_eq $P/patch-semver "3" "$(pv patch_semver)" check_eq $P/micro-is-patch "3" "$(pv micro_is_patch)" check_eq $P/year-is-major "2024" "$(pv year_is_major)" check_eq $P/major-buildver "7" "$(pv major_buildver)" check_eq $P/minor-buildver "None" "$(pv minor_buildver)" check_eq $P/patch-buildver "None" "$(pv patch_buildver)" check_eq $P/major-container "None" "$(pv major_container)" check_eq $P/major-unknown "None" "$(pv major_unknown)" # `sections' and `section(idx)'. A SemVer is hardwired to 3 sections; an index # past the end returns 0 rather than raising; and HexVer is a special case that # returns the whole value at index 0 and 0 everywhere else. check_eq $P/sections-semver "3" "$(pv sections_semver)" check_eq $P/sections-simple "4" "$(pv sections_simple)" check_eq $P/sections-buildver "1" "$(pv sections_buildver)" check_eq $P/section-0 "1" "$(pv section_0)" check_eq $P/section-2 "3" "$(pv section_2)" check_eq $P/section-past-end "0" "$(pv section_past_end)" check_eq $P/section-hexver "31" "$(pv section_hexver)" check_eq $P/section-hexver-1 "0" "$(pv section_hexver_1)" # --------------------------------------------------------------------------- # LAYER 3e -- EQUALITY, HASHING, AND USE AS A DICT KEY. # # AwesomeVersion subclasses `str' and hashes on `.string' -- the body WITHOUT # the prefix -- so `v1.0.0' and `1.0.0' are ONE key, and a plain `str' # retrieves it. A caller keying a dict on versions depends on all three of # those facts and not one of them is obvious from the API. # --------------------------------------------------------------------------- check_eq $P/hash-eq-prefixed "yes" "$(pv hash_eq_prefixed)" check_eq $P/hash-eq-plain-str "yes" "$(pv hash_eq_plain_str)" check_eq $P/hash-ne-different "yes" "$(pv hash_ne_different)" check_eq $P/dict-one-key "1" "$(pv dict_one_key)" check_eq $P/dict-value "second" "$(pv dict_value)" check_eq $P/dict-by-plain-str "second" "$(pv dict_by_plain_str)" check_eq $P/set-dedups "2" "$(pv set_dedups)" # `repr' and `str' DISAGREE about the prefix: `str' keeps it, `repr' shows the # stripped body. Stated as a pair, because a reader who checks one will assume # the other. check_eq $P/repr-semver '' "$(pv repr_semver)" check_eq $P/repr-prefixed '' "$(pv repr_prefixed)" check_eq $P/str-semver "1.2.3" "$(pv str_semver)" check_eq $P/str-keeps-prefix "v1.2.3" "$(pv str_keeps_prefix)" # Construction from an AwesomeVersion, an int and a float, and the two # normalisations the constructor performs silently. check_eq $P/from-awesomeversion "1.2.3" "$(pv from_awesomeversion)" check_eq $P/from-int "1" "$(pv from_int)" check_eq $P/from-float "1.2" "$(pv from_float)" check_eq $P/strips-whitespace "1.2.3" "$(pv strips_whitespace)" check_eq $P/strips-trailing-dot "1.2.3" "$(pv strips_trailing_dot)" # The context-manager protocol, which the class implements and nothing # documents. check_eq $P/context-manager "1.2.3" "$(pv context_manager)" # --------------------------------------------------------------------------- # LAYER 3f -- diff / __sub__, a structured description of what changed. # --------------------------------------------------------------------------- check_eq $P/diff-type "AwesomeVersionDiff" "$(pv diff_type)" check_eq $P/diff-major "yes" "$(pv diff_major)" check_eq $P/diff-minor "no" "$(pv diff_minor)" check_eq $P/diff-patch "no" "$(pv diff_patch)" check_eq $P/diff-modifier "no" "$(pv diff_modifier)" check_eq $P/diff-strategy "no" "$(pv diff_strategy)" check_eq $P/sub-is-diff "yes" "$(pv sub_is_diff)" check_eq $P/sub-major "yes" "$(pv sub_major)" check_eq $P/diff-strategy-changed "yes" "$(pv diff_strategy_changed)" check_eq $P/diff-repr "AwesomeVersionDiff(major=True, minor=False, patch=False, modifier=False, strategy=False)" "$(pv diff_repr)" # --------------------------------------------------------------------------- # LAYER 3g -- ERROR PATHS AND THE ensure_strategy TRAP. # # Each is asserted as the documented exception TYPE, because a caller writes # `except' against the type and not against the message. # # ensure_strategy DOES NOT MERELY VALIDATE -- IT REORDERS THE MATCH, and that is # the most consequential surprise in this library. `strategy' walks the ensure # list FIRST and the fixed tuple afterwards, so naming a strategy can CHANGE the # answer rather than assert it: `2024.1.0' is CalVer unasked and SemVer when # SemVer is named, because it matches BOTH patterns and the ensure list moved # SemVer in front of CalVer. It raises only when NOTHING in the list matches. # The four cases below state both halves, so the distinction is on the record # rather than inferred from whichever one a reader tries first. # --------------------------------------------------------------------------- check_eq $P/ensure-ok "1.2.3" "$(pv ensure_ok)" check_eq $P/ensure-reorders-value "2024.1.0" "$(pv ensure_reorders_value)" check_eq $P/ensure-reorders-strategy "SemVer" "$(pv ensure_reorders_strategy)" check_eq $P/unasked-strategy "CalVer" "$(pv unasked_strategy)" check_eq $P/ensure-real-mismatch "EXC:AwesomeVersionStrategyException" "$(pv ensure_real_mismatch)" check_eq $P/ensure-calver-on-semver "EXC:AwesomeVersionStrategyException" "$(pv ensure_calver_on_semver)" check_eq $P/ensure-list-ok "2024.1.0" "$(pv ensure_list_ok)" check_eq $P/ensure-list-mismatch "EXC:AwesomeVersionStrategyException" "$(pv ensure_list_mismatch)" check_eq $P/ensure-unknown-refused "EXC:AwesomeVersionStrategyException" "$(pv ensure_unknown_refused)" # `find_first_match' scans a longer string for the first substring matching the # named strategy, and raises when there is none. check_eq $P/find-first-match "1.2.3" "$(pv find_first_match)" check_eq $P/find-first-match-absent "EXC:AwesomeVersionStrategyException" "$(pv find_first_match_absent)" # COMPARING AN UNKNOWN RAISES rather than returning an arbitrary answer -- the # case the ticket names, asserted in BOTH directions. But EQUALITY of two # unknowns does NOT raise: it is a plain string comparison, so `==' and `<' # disagree about whether an unparseable version is usable at all. check_eq $P/cmp-unknown-lt "EXC:AwesomeVersionCompareException" "$(pv cmp_unknown_lt)" check_eq $P/cmp-unknown-gt "EXC:AwesomeVersionCompareException" "$(pv cmp_unknown_gt)" check_eq $P/cmp-unknown-rhs "EXC:AwesomeVersionCompareException" "$(pv cmp_unknown_rhs)" check_eq $P/eq-unknown-same "True" "$(pv eq_unknown_same)" check_eq $P/eq-unknown-diff "False" "$(pv eq_unknown_diff)" # A comparison against something that is not a version at all. `== None' RAISES # -- it does not return False, which is what every other object in Python does # and what a caller writing `if version == None' will assume. Both `==' and # `!=' are stated, because an equality test that throws is not something an # `except' around a COMPARISON would catch. Membership, by contrast, DOES work # against plain strings, since `in' uses `__eq__' and a str is coerced. check_eq $P/cmp-vs-none "EXC:AwesomeVersionCompareException" "$(pv cmp_vs_none)" check_eq $P/cmp-vs-list "EXC:AwesomeVersionCompareException" "$(pv cmp_vs_list)" check_eq $P/eq-vs-none "EXC:AwesomeVersionCompareException" "$(pv eq_vs_none)" check_eq $P/ne-vs-none "EXC:AwesomeVersionCompareException" "$(pv ne_vs_none)" check_eq $P/in-str-list "True" "$(pv in_str_list)" check_eq $P/diff-vs-none "EXC:AwesomeVersionCompareException" "$(pv diff_vs_none)" check_eq $P/in-range-bad-low "EXC:AwesomeVersionCompareException" "$(pv in_range_bad_low)" check_eq $P/in-range-bad-high "EXC:AwesomeVersionCompareException" "$(pv in_range_bad_high)" # --------------------------------------------------------------------------- # LAYER 3h -- TWO THINGS THE LIBRARY DOES THAT A READER WOULD NOT PREDICT. # # Both are pure Python and present on every platform this release runs on; # NEITHER IS A brintOS PLATFORM GAP, and per `no-public-upstream-reports' # nothing is filed against the third-party repository. They are asserted AS # SHIPPED, so they pass here and go red the day the library changes -- which is # the signal wanted. # # The deprecated POSITIONAL ensure_strategy still works and warns; the warning # text says it "will be removed in 2023", and this is the 2024.2.0 release, so # the deprecation has outlived its own deadline by a year. # --------------------------------------------------------------------------- check_eq $P/positional-still-works "1.2.3" "$(pv positional_still_works)" check_eq $P/positional-warns "yes" "$(pv positional_warns)" # `find_first_match' WITHOUT `ensure_strategy' warns and is otherwise IGNORED -- # it does not raise, and it does not scan. The version comes back as the whole # input string, which is then UNKNOWN. check_eq $P/ffm-without-ensure "version 1.2.3 here" "$(pv ffm_without_ensure)" check_eq $P/ffm-without-ensure-warns "yes" "$(pv ffm_without_ensure_warns)" # --------------------------------------------------------------------------- # LAYER 3i -- NOTHING THIS LIBRARY DOES REACHES THE OUTSIDE. # # Asserted rather than assumed: it is the claim that lets this port close on # its suite with no BRINTOS-780 follow-on, and it costs three cases to state. # After the whole probe has run, none of `socket', `subprocess' or `logging' is # in `sys.modules' -- so the library opened no connection, spawned nothing, and # (the recipe's own RDEPENDS notwithstanding) never touched logging. # --------------------------------------------------------------------------- check_eq $P/no-socket-module "yes" "$(pv no_socket_module)" check_eq $P/no-subprocess-module "yes" "$(pv no_subprocess_module)" check_eq $P/no-logging-module "yes" "$(pv no_logging_module)" rm -rf "$W"