. "$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 sixty-nine measurements. # # # # The work inside each start is a handful of `re.match' # # calls over strings under 40 bytes and the `datetime' # # constructions 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-wcwidth 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-iso8601 2.1.0 (poky/meta # recipes-devtools/python/python3-iso8601_2.1.0.bb, `inherit pypi # python_poetry_core', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # `iso8601.parse_date(s)' turns an ISO 8601 timestamp into a timezone-aware # `datetime.datetime'. That is very nearly the whole of it: five exported names, # one regex, and one constructor call. There is no configure step, no compiled # extension and no C anywhere in the package. # # What it supplies for the fields the input LEAVES OUT is the part worth reading # from the cases rather than assuming. A missing month and day become 1, a # missing time becomes midnight, and A MISSING ZONE BECOMES UTC -- none of which # appears in the input. `form_year_only' through `form_date_only' pin all three, # because a caller who read `2007' as "a year" and got a UTC midnight instant # has been given an answer the string did not contain. # # ============================================================================ # `is_iso8601' IS NOT VALIDATION, AND THAT IS THIS SUITE'S MOST USEFUL SINGLE # STATEMENT. # ============================================================================ # # `is_iso8601(s)' runs THE REGEX AND NOTHING ELSE. It never constructs the # `datetime', so it never learns whether the numbers the regex captured are a # date. Four inputs `parse_date' refuses come back True from it: # # 2007-13-01 month 13 # 2007-01-25T25:00:00Z hour 25 # 2015-02-29 February 29th in a common year # 2007-01-25T12:00:00+99:99 an offset of ninety-nine hours # # A caller using it as a gate before storing a string lets all four through, and # then raises `ParseError' at the point of use rather than at the point of # validation. The `isiso-*' cases assert each of those against the `err-*' case # for the SAME input: the divergence is the finding and either half alone is # unremarkable. # # It is not simply broken, either -- `2007/01/25', `200701', `not a date' and the # empty string all come back False, so the cheap syntactic answer is correct as # far as a regex can be. The cases pin both halves so a reader can see exactly # where the line falls. # # ============================================================================ # `FixedOffset' IS A FUNCTION AND `UTC' IS `datetime.timezone.utc', WHICH THE # MODULE'S OWN DOCSTRING STILL CONTRADICTS. # ============================================================================ # # Before 2.0 this package carried its own `Utc' and `FixedOffset' tzinfo # CLASSES. It no longer does: `UTC' is bound straight to `datetime.timezone.utc' # and `FixedOffset' is a three-argument FUNCTION returning a # `datetime.timezone'. Upstream never updated the module docstring, which still # shows `tzinfo=' in its example. # # So the documented answer is the wrong one, and a caller who followed the # docstring and wrote `isinstance(d.tzinfo, iso8601.Utc)' has nothing to import. # `fixedoffset-kind', `fixedoffset-returns' and `utc-is-timezone-utc' pin what # is actually there, precisely because the docstring makes that worth stating. # # ============================================================================ # THE SEVENTH FRACTIONAL DIGIT IS DISCARDED, NOT ROUNDED. # ============================================================================ # # Microseconds come from `int(Decimal("0.") * Decimal("1000000.0"))'. The # `Decimal' arithmetic is exact and `int()' TRUNCATES, so `.1234567' and # `.1234569' are the same microsecond -- 123456 -- and `frac_over' asserts that # literal. A reader who assumed rounding would expect 123457. # # ============================================================================ # THE PACKAGE SHIPS A SUBMODULE THIS IMAGE CANNOT IMPORT, AND IT DOES NOT # MATTER. # ============================================================================ # # Upstream keeps its pytest suite INSIDE the package, so `test_iso8601.py' and # its precompiled `.pyc' land in site-packages; poetry-core packages the whole # `iso8601' directory and `pyproject.toml' excludes nothing. It imports `pytest' # and `hypothesis' at module level and this image carries neither. # # `test-module-ships' asserts the file is there and `test-module-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' above passes regardless. # # ============================================================================ # WHY THE FAILURE CASES ASSERT AN EXCEPTION NAME. # ============================================================================ # # `parse_date' wraps everything it catches in `ParseError', including the # `ValueError' that `datetime.datetime()' raises for an out-of-range field. So # one exception type covers both a string the regex refused and a string it # accepted whose numbers are not a date, and the NAME is the whole of the # assertion: a release that let the underlying `ValueError' escape would break # every caller's `except iso8601.ParseError' and change nothing else a # behavioural case could see. `parseerror-base' and `caught-as-valueerror' pin # the other direction -- it subclasses `ValueError', so the built-in still # catches it. P=python3-iso8601 # 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/iso8601 DI=$SITE/iso8601-2.1.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-iso8601.list PROBE=$BST_DATA/python3-iso8601-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-iso8601.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- where it lands --- # THE SHAPE IS A PACKAGE DIRECTORY, not a single module -- the opposite of # python3-smartypants, and it is what `py.typed' and the shipped test submodule # hang off. Asserted directly because a release that flattened to one file would # still import and still pass every behavioural case in this file. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/no-flat-module test ! -f "$SITE/iso8601.py" check $P/init-file test -s "$PKG/__init__.py" check $P/submodule-file test -s "$PKG/iso8601.py" # PEP 561: an EMPTY marker file, which is why this is `-f' and not `-s'. It is # what makes the shipped type annotations visible to a checker, and a packaging # change that dropped it would leave the annotations in place and unused. check $P/py-typed-present test -f "$PKG/py.typed" check_eq $P/py-typed-empty "0" "$(wc -c < "$PKG/py.typed" 2>/dev/null | tr -d ' ')" # Upstream's own pytest suite, shipped. See the header: it is dead weight on this # image, and `test-module-not-importable' below says so from the other side. check $P/test-module-ships test -s "$PKG/test_iso8601.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-mod test -s "$PKG/__pycache__/iso8601.cpython-312.pyc" check $P/pycache-test test -s "$PKG/__pycache__/test_iso8601.cpython-312.pyc" # --------------------------------------------------------- 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). 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', # the PEP 561 marker 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 ;; */py.typed) 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.1.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: iso8601" 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. `python_poetry_core' is # what distinguishes this recipe from the `setuptools3' ports beside it, and the # `Generator' line is the only place on the image that records which backend ran. # A recipe that silently fell back to setuptools would still install and still # import; this is the case that would notice. check_out $P/dist-info-generator "Generator: poetry-core" 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" # poetry-core does NOT emit a `top_level.txt' -- that is a setuptools artifact. # Asserted as an absence so a reader porting the neighbouring setuptools suites # is told this is the backend's doing rather than a packaging loss. check $P/dist-info-no-top-level test ! -e "$DI/top_level.txt" # --------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-iso8601$/,/^$/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.1.0-r0" cat "$W/st" # THE DEPENDENCY SET, EXACTLY. The recipe's RDEPENDS names `python3-datetime' and # `python3-numbers' -- the latter is what `decimal' lives in, and `decimal' is how # the fractional second is converted. `python3-core' is added by the packaging. # All three are sub-packages of the ported python3 (BRINTOS-1476), so this port # waits on no other package, and asserting the WHOLE line rather than a substring # is what would catch a fourth name appearing. 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-datetime, python3-numbers" \ "$(cat "$W/dep")" # ------------------------------------------------------------------ it imports -- check $P/import python3 -c 'import iso8601' # ...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. check_eq $P/import-quiet "" \ "$(python3 -c 'import iso8601' 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 "69" "$(pv emitted_count)" # -------------------------------------------------------------- the identity --- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # package named here; without this one 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/submodule-answered "$PKG/iso8601.py" "$(pv submodule_file)" check_eq $P/is-package "yes" "$(pv is_package)" # The version read through importlib.metadata, i.e. off the dist-info the # `dist-info-version' case read as text. The package exposes no `__version__' of # its own, so this is the only version statement the RUNTIME can make. check_eq $P/dist-version "2.1.0" "$(pv dist_version)" # THE EXPORTED SURFACE, in order. `__init__.py' re-exports five names and lists # them in `__all__', so `from iso8601 import *' is defined by this line. check_eq $P/all-names "parse_date,is_iso8601,ParseError,UTC,FixedOffset" \ "$(pv all_names)" check_eq $P/all-count "5" "$(pv all_count)" # Spelled as id/name pairs rather than derived with `tr': the case id and the # probe key differ by case as well as by separator, and mixing a literal `_' with # `[:upper:]' in one `tr' set is implementation-defined -- on the target's ash it # could yield garbled ids for no benefit. for _p in has-parse-date:parse_date has-is-iso8601:is_iso8601 \ has-parseerror:ParseError has-utc:UTC has-fixedoffset:FixedOffset; do check_eq $P/${_p%%:*} "yes" "$(pv has_${_p#*:})" done # ----------------------------------------------------------------- the types --- # See the header: the module docstring still describes the pre-2.0 classes, so # what is actually there is worth pinning. check_eq $P/fixedoffset-kind "function" "$(pv fixedoffset_kind)" check_eq $P/fixedoffset-returns "timezone" "$(pv fixedoffset_returns)" check_eq $P/fixedoffset-offset "9000" "$(pv fixedoffset_offset)" check_eq $P/fixedoffset-name "+02:30" "$(pv fixedoffset_name)" check_eq $P/utc-is-timezone-utc "yes" "$(pv utc_is_timezone_utc)" check_eq $P/utc-tzname "UTC" "$(pv utc_tzname)" # `ParseError' subclasses `ValueError', which is what lets a caller who never # imported this package still catch a bad date with the built-in. check_eq $P/parseerror-base "ValueError" "$(pv parseerror_base)" check_eq $P/caught-as-valueerror "ParseError" "$(pv caught_as_valueerror)" # ------------------------------------------------- every field, once, apart --- # ONE INPUT WITH NO TWO FIELDS ALIKE -- 2007-03-04T05:06:07.008009+02:30 -- so a # transposition is visible. These nine are asserted separately rather than as one # joined string: a joined line reds as a single case whichever field moved, and # year-through-microsecond plus the offset is the whole of what this package # returns. check_eq $P/full-year "2007" "$(pv full_year)" check_eq $P/full-month "3" "$(pv full_month)" check_eq $P/full-day "4" "$(pv full_day)" check_eq $P/full-hour "5" "$(pv full_hour)" check_eq $P/full-minute "6" "$(pv full_minute)" check_eq $P/full-second "7" "$(pv full_second)" check_eq $P/full-microsecond "8009" "$(pv full_microsecond)" check_eq $P/full-offset "9000" "$(pv full_offset)" check_eq $P/full-tzname "+02:30" "$(pv full_tzname)" # ------------------------------------------------------------------ the forms --- # Each documented input shape, as `Y-M-D H:M:S.uuuuuu||'. # The fields are asserted apart above; what these add is that each SHAPE reaches # the same parse, which is a statement about the regex rather than about # `datetime'. # # THE FIRST THREE ARE THE ONES TO READ. Nothing in `2007' says January, midnight # or UTC, and all three come back anyway -- so a caller who parsed a bare year is # holding an instant the string did not contain. check_eq $P/form-year-only "2007-01-01 00:00:00.000000|0|UTC" "$(pv form_year_only)" check_eq $P/form-year-month "2007-01-01 00:00:00.000000|0|UTC" "$(pv form_year_month)" check_eq $P/form-date-only "2007-01-25 00:00:00.000000|0|UTC" "$(pv form_date_only)" # The basic format, without separators. `20070125' parses; `200701' does NOT, and # that asymmetry is asserted on the failure path below -- it is deliberate, not a # gap. check_eq $P/form-basic-date "2007-01-25 00:00:00.000000|0|UTC" "$(pv form_basic_date)" check_eq $P/form-basic-time "2007-01-25 12:00:00.000000|0|UTC" "$(pv form_basic_time)" # Date and time. A SPACE is as valid a separator as `T' here, which surprises # callers who validate against the letter. check_eq $P/form-datetime-z "2007-01-25 12:00:00.000000|0|UTC" "$(pv form_datetime_z)" check_eq $P/form-datetime-naive-default "2007-01-25 12:00:00.000000|0|UTC" \ "$(pv form_datetime_naive_default)" check_eq $P/form-datetime-space "2007-01-25 12:00:00.000000|0|UTC" \ "$(pv form_datetime_space)" # Fractional seconds at three precisions, and with the comma the standard permits # as a decimal mark. `frac-over' is the truncation case from the header. check_eq $P/form-frac-half "2007-01-25 12:00:00.500000|0|UTC" "$(pv form_frac_half)" check_eq $P/form-frac-milli "2007-01-25 12:00:00.123000|0|UTC" "$(pv form_frac_milli)" check_eq $P/form-frac-over "2007-01-25 12:00:00.123456|0|UTC" "$(pv form_frac_over)" check_eq $P/form-frac-comma "2007-01-25 12:00:00.500000|0|UTC" "$(pv form_frac_comma)" # The offsets. `tz-hour-only' has no minute field at all, which the regex allows # and which resolves to :00. check_eq $P/form-tz-plus "2007-01-25 12:00:00.000000|19800|+05:30" "$(pv form_tz_plus)" check_eq $P/form-tz-minus "2007-01-25 12:00:00.000000|-18000|-05:00" "$(pv form_tz_minus)" check_eq $P/form-tz-hour-only "2007-01-25 12:00:00.000000|3600|+01:00" \ "$(pv form_tz_hour_only)" # A LEAP DAY on a year that has one. Its pair on the failure path (`2015-02-29') # is what makes this case mean anything: the regex cannot tell them apart, and # `datetime' is what rejects the second. check_eq $P/form-leap-day "2016-02-29 00:00:00.000000|0|UTC" "$(pv form_leap_day)" # THE NAME IS BUILT BEFORE THE SIGN IS APPLIED, which is why a negative offset's # `tzname()' reads `-05:00' rather than the `-1 day, 19:00:00' a caller printing # the `timedelta' would see. This is the one place the package formats anything. check_eq $P/tz-minus-name "-05:00" "$(pv tz_minus_name)" # ------------------------------------------------- the default-timezone arg --- # BOTH DOCUMENTED MODES, plus the case that says the argument is a DEFAULT and # not an override. check_eq $P/dtz-none-is-naive "yes" "$(pv dtz_none_is_naive)" check_eq $P/dtz-none "2007-01-25 12:00:00.000000|naive|" "$(pv dtz_none)" check_eq $P/dtz-supplied "2007-01-25 12:00:00.000000|9000|+02:30" "$(pv dtz_supplied)" # An input that names its own zone is NOT overridden. Without this case the two # above would pass on an implementation that ignored the string's zone entirely. check_eq $P/dtz-explicit-wins "2007-01-25 12:00:00.000000|0|UTC" \ "$(pv dtz_explicit_wins)" # ----------------------------------------------------------- the failure path --- # EVERY REFUSAL IS A `ParseError', whatever raised underneath. See the header. for _k in wrong_separator month_range hour_range leap_common_year \ impossible_offset yyyymm not_a_date empty; do check_eq $P/err-$(echo "$_k" | tr _ -) "ParseError" "$(pv err_$_k)" done # ------------------------------------------- is_iso8601 is not validation --- # The four inputs where the two functions DISAGREE, each asserted against its # `err-*' case directly above. See the header: this is the suite's most useful # single statement, and a caller using `is_iso8601' as a storage gate lets all # four through. check_eq $P/isiso-month-range "yes" "$(pv isiso_month_range)" check_eq $P/isiso-hour-range "yes" "$(pv isiso_hour_range)" check_eq $P/isiso-leap-common-year "yes" "$(pv isiso_leap_common_year)" check_eq $P/isiso-impossible-offset "yes" "$(pv isiso_impossible_offset)" # ...and the ones where they agree, which is what says the regex is not simply # permissive. Without these the four above would read as "the function is # broken" rather than as "it answers a syntactic question". check_eq $P/isiso-valid "yes" "$(pv isiso_valid)" check_eq $P/isiso-wrong-separator "no" "$(pv isiso_wrong_separator)" check_eq $P/isiso-yyyymm "no" "$(pv isiso_yyyymm)" check_eq $P/isiso-not-a-date "no" "$(pv isiso_not_a_date)" check_eq $P/isiso-empty "no" "$(pv isiso_empty)" # --------------------------------- the submodule that ships and cannot run --- # See the header. `test-module-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/test-module-not-importable "ModuleNotFoundError" \ "$(pv test_module_imports)" rm -rf "$W"