. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those three. # # # # They are a bare `import', a second one whose STDERR is # # the assertion (`import-quiet'), and the probe -- which # # takes ONE start for a hundred and ninety-six # # measurements. The work inside the probe is string # # parsing over inputs under 40 bytes, 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 budget 99-python3-parse # # and 99-python3-wcwidth set for their three, and roughly # # six times the ~60s a python3 start has measured at here. # # # # 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-aniso8601 9.0.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-aniso8601_9.0.1.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # An ISO 8601 parser that covers the parts of the standard a timestamp parser # usually leaves out. `datetime.fromisoformat' handles a calendar date and a # time; this handles ORDINAL dates (`1981-095'), WEEK dates (`2004-W53-6'), # DURATIONS (`P3Y6M4DT12H30M5S'), INTERVALS (`/' and its two # duration spellings) and REPEATING intervals (`R3/...', `R/...'), each behind # its own entry point and each with its own resolution query. # # Six parse functions and six resolution functions, all re-exported from the # package root, and the suite asserts every one of the twelve is reachable # there -- `api-*' -- before it asserts anything about what they return. # # ============================================================================ # THE WEEK-NUMBERING YEAR IS NOT THE CALENDAR YEAR, AND THAT IS THE ONE THING # A CALLER GETS WRONG. # ============================================================================ # # `2004-W53-6' is the 1st of January 2005. `2004-W01-1' is the 29th of December # 2003. `2009-W53-7' is the 3rd of January 2010, and `2010-W01-1' is the 4th of # January -- not the 1st. Four cases, chosen so the year in the string differs # from the year on the calendar in BOTH directions and so the "week 1 starts on # the 1st" reading fails on the fourth. # # A caller who reads the `YYYY' of a week date as the calendar year is wrong by # up to three days, silently, and only in some years -- which is why it is # worth four cases rather than one. `week-agrees-isocalendar' closes the group # by round-tripping the answer through the INTERPRETER's own ISO calendar, so # the assertion is about agreement rather than about a literal somebody typed. # # ============================================================================ # A YEAR IS 365 DAYS AND A MONTH IS 30, WHICH MAKES `P12M' SHORTER THAN `P1Y'. # ============================================================================ # # `parse_duration' returns a `datetime.timedelta', and a timedelta cannot carry # a calendar year: February and July are the same length in it, and so are 2023 # and 2024. So the builder converts with DAYS_PER_YEAR = 365 and # DAYS_PER_MONTH = 30 -- flat, leap years ignored. `P3Y6M4DT12H30M5S' is # therefore 1279 days and 45005 seconds, and it is 1279 for that reason and no # other. # # The two constants are read off the builder directly (`const-days-per-year', # `const-days-per-month') as well as being implied by every arithmetic case, so # a release that changed one is NAMED here rather than only causing a spray of # reds. And `twelve-months-vs-year' asserts the corollary a caller does not # expect: 12 * 30 is 360, so `P12M' and `P1Y' are DIFFERENT durations. # # This is the "documented handling of years and months" the ticket asks to be # asserted explicitly rather than assumed, and it is asserted three ways: the # constants, the arithmetic, and the inequality. # # ============================================================================ # `/' COMES BACK REVERSED, ON PURPOSE. # ============================================================================ # # `parse_interval("P1M/1981-04-05")' returns `(1981-04-05, 1981-03-06)' -- the # END first. Upstream says so in a comment and gives the reason ("to maintain # consistency with parsing / durations, as well as making repeating # interval code cleaner") and the remedy (`sorted'). A caller who unpacks it as # `start, end' gets the two backwards and NOTHING RAISES. # # `interval-dur-end-reversed' asserts the inversion as a relation rather than # only through the two literals, so the case says what it means. # # The same anchor-at-the-end rule governs `R3/P1D/', which walks BACKWARDS # -- `repeat-backwards'. # # ============================================================================ # AN INTERVAL'S TWO HALVES NEED NOT BE THE SAME TYPE. # ============================================================================ # # `1980-03-05/P1DT4H' returns `(date, datetime)': the start stays a date # because that is what the string said, and the end is promoted because the # duration has time in it. Subtracting one from the other is a TypeError. Every # interval case therefore asserts the PAIR OF TYPE NAMES beside the values -- # `*-types' -- because a caller who assumes homogeneity is right about four of # the six forms. # # ============================================================================ # BOTH REPEATING FORMS ARE GENERATORS, THE BOUNDED ONE INCLUDED. # ============================================================================ # # `R3/...' is not a list of three. `len()' on it raises TypeError # (`repeat-bounded-len'), and so does indexing it. The unbounded `R/...' is # taken three values at a time and never with `list()' -- `repeat-unbounded- # first3' plus `repeat-unbounded-fourth', which together say "still going" # rather than "happened to have three". Nothing in this suite materialises an # unbounded generator, which would hang the guest and cost every member of the # batch its round. # # `R0' is an EMPTY generator rather than an error, which is the boundary a # caller looping over the result meets first. # # ============================================================================ # THE EXCEPTION TREE IS NOT THE SHAPE THE TICKET'S NAME FOR IT SUGGESTS, AND # THIS IS THE PORT'S MOST USEFUL SINGLE FINDING. # ============================================================================ # # The ticket asks for "the specific `ISOFormatError' subclass the module # documents for malformed input". MEASURED: `ISOFormatError' HAS NO SUBCLASSES. # The nine range errors -- YearOutOfBoundsError, MonthOutOfBoundsError, # DayOutOfBoundsError, WeekOutOfBoundsError, HoursOutOfBoundsError, # MinutesOutOfBoundsError, SecondsOutOfBoundsError, MidnightBoundsError, # LeapSecondError -- descend from `RangeCheckError', which is a SIBLING of # ISOFormatError. The two hierarchies meet only at `ValueError'. # # So the tree is two branches, and which one you get is decided by WHAT IS # WRONG rather than by which function you called: # # malformed -> ISOFormatError (`err-*' below, 14 cases) # out of range -> a RangeCheckError subclass (7 cases, one per class) # # A caller writing `except ISOFormatError' to mean "bad input" catches the # first branch and lets every out-of-range value through. `isoformat- # subclasses' and `range-under-isoformat' assert the shape directly, so this # stays a measured statement rather than a note in a comment. # # TWO REFUSALS ARE IN NEITHER BRANCH, and one of them is not even a ValueError: # # a NON-STRING argument -> bare `ValueError', raised before any parsing # the EXTENDED YEAR (`+2000') -> `NotImplementedError' # # `NotImplementedError' is a RuntimeError, so `except ValueError' does not # catch it and the process dies on input that is perfectly well-formed ISO # 8601. `err-extended-is-valueerror' asserts that negative explicitly, because # it is the one a caller cannot guess. # # ============================================================================ # THE RUNTIME DEPENDENCY THE STOCK RECIPE DOES NOT DECLARE. # ============================================================================ # # The built .deb reads `Depends: python3-core' and nothing else -- # setuptools3-base's inherited entry, with the recipe adding none of its own. # That is not enough to import the package. `aniso8601/builders/__init__.py' # does a module-scope `import calendar' and `builders/python.py' and # `utcoffset.py' do `import datetime'; poky's python3-manifest.json puts BOTH # calendar.py and datetime.py in python3-DATETIME, not in python3-core. # # MEASURED, not read off the manifest: with a meta_path hook refusing calendar # and datetime exactly as an image without that split would, `import aniso8601' # raises `ModuleNotFoundError: No module named 'calendar'' -- at the FIRST # stdlib import in the tree, before any parsing code is reached. # # python3-aniso8601_%.bbappend adds the one entry, and `pkg-depends-datetime' # below asserts dpkg recorded it. On this image the omission would have been # invisible -- python3-datetime is in IMAGE_INSTALL for other reasons -- which # is exactly why it is asserted rather than left to be discovered by whoever # first takes this .deb onto a smaller rootfs. # # ============================================================================ # THE UPSTREAM TEST SUITE IS INSIDE THE RUNTIME PACKAGE, AND THAT IS UNUSUAL. # ============================================================================ # # setup.py uses a bare `find_packages()', so `aniso8601.tests' and # `aniso8601.builders.tests' are packaged as ordinary subpackages -- 200 kB of # unittest sources and their bytecode ship in the RUNTIME .deb, on every image # that installs it. This recipe does not `inherit ptest' and no -ptest package # exists, so unlike python3-parse and python3-wcwidth there is no split that # was declined here; the tests are simply in the wrong package upstream. # # `tests-dir-present' asserts they are there rather than pretending they are # not, and `tests-not-imported' asserts the cost is disk and nothing else: the # test modules are NOT reached by `import aniso8601', so they add no runtime # dependency (they import `unittest' and, on the builders side, `mock') and # cannot fail an import. # # This suite does not run them. They are 200 kB of unittest across nine files # and the entry point is `python3 -m unittest', which is a fourth interpreter # start plus the whole discovery machinery inside this suite's 1200 s bound -- # on an arm where a start alone is ~60 s. The cases below reproduce upstream's # own fixtures without the framework, which is the same decision already # recorded for python3-parse, taken here for cost rather than for packaging. # A follow-on that runs them is a separate ticket with its own bound. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the tree UNPACKED FROM THE # BUILT python3-aniso8601_9.0.1-r0_wasm32.deb -- not against the sdist, and not # against a pip install. The unpacked tree was additionally compared to the # sdist the recipe fetches (sha256 72e3117667eedf66951bb2d93f4296a56b94b078a8a # 95905a052611fb3f1b973, which is the recipe's own SRC_URI[sha256sum], verified # by digest) and is BYTE-IDENTICAL to it -- so "no patch" here is a measurement # rather than a reading of the recipe. # # The control interpreter is CPython 3.12.3 and the image ships 3.12, so the # two agree on minor version. That matters for exactly one group: `pulled- # nonstdlib' is scored against `sys.stdlib_module_names', which exists from # 3.10. # # The suite was then run END TO END on x86-64 against that same tree, with the # site-packages, dpkg status and dpkg file-list paths redirected at it. # # The on-target result is not yet measured -- the batch verify is this port's # first run on wasm32-hwjs. P=python3-aniso8601 # THE CANARY, which asserts nothing about python3-aniso8601. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/aniso8601 DI=$SITE/aniso8601-9.0.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-aniso8601.list PROBE=$BST_DATA/python3-aniso8601-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-aniso8601.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import aniso8601' further down is the # functional statement; these are the ones that say WHICH copy answered, so a # package that imported out of some other path on sys.path is distinguishable # from one that imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A MODULE. setup.py uses `find_packages()', so a package # tree lands in site-packages -- the opposite shape from python3-parse's single # file. This is the case that fails a release which flattened to one module, # loudly and here, rather than nowhere. check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/aniso8601.py" check $P/init-file test -s "$PKGDIR/__init__.py" # The submodule that does the arithmetic, and the one that holds the exception # tree. Named because the two headline findings above are about them. check $P/builders-python test -s "$PKGDIR/builders/python.py" check $P/exceptions-py test -s "$PKGDIR/exceptions.py" # The precompiled bytecode, which is what `import-quiet' below turns on. check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # NO SHIPPED SCRIPT. This package is a library and installs no executable. # Asserted so a release that grew a console_scripts entry point announces # itself here. check $P/no-cli test ! -e /usr/bin/aniso8601 # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. On this image a .so # could not be loaded at all -- python3 is ONE STATIC BINARY with an empty # lib-dynload and no dynamic loader (BRINTOS-1476) -- so this is the case that # fails a version bump which grew one, at install rather than at `import'. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case # above names the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE UPSTREAM TEST SUITE IS IN THE RUNTIME PACKAGE -- see the header. This is # the opposite of the assertion python3-parse makes at the same point, and the # difference is real: there the tests went to a -ptest package this image does # not build, here `find_packages()' put them in the runtime tree. Asserted # positively so a release that MOVED them announces itself, rather than being # absent from the suite because it was inconvenient. check $P/tests-dir-present test -d "$PKGDIR/tests" check $P/builders-tests-dir-present test -d "$PKGDIR/builders/tests" check $P/tests-are-python test -s "$PKGDIR/tests/test_date.py" # ----------------------------------------------------------- the dist-info --- # The installed metadata. `__init__.py' carries `__version__' and setup.py # imports it (`from aniso8601 import __version__'), so the two cannot disagree # at build time -- which makes `version-agrees' below a statement about the # INSTALL rather than about upstream: a tree landing beside a dist-info from # some other release is what it catches. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 9.0.1" cat "$DI/METADATA" check_out $P/dist-info-name "Name: aniso8601" cat "$DI/METADATA" # The one-line description, which is upstream's own and is the shortest true # statement of what the package is. It is also the recipe's DESCRIPTION. check_out $P/dist-info-summary "A library for parsing ISO 8601 strings." cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import aniso8601' has to match, and a wheel that renamed the tree would # change it here first. check_out $P/dist-info-top-level "aniso8601" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points (BSD-3-Clause). check $P/dist-info-licence test -s "$DI/LICENSE" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-aniso8601 is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-aniso8601$/,/^$/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" # THE RUNTIME DEPENDENCIES, as dpkg recorded them -- and this is the case the # bbappend exists for. The stock recipe declares python3-core alone; the tree # imports `calendar' and `datetime' at module scope and BOTH are in # python3-datetime. Each is asserted by name, because a Depends line that lost # one would still install on THIS image (python3-datetime is here for other # reasons) and break on the first rootfs that took the .deb alone. 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" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" # NO -ptest PACKAGE EXISTS FOR THIS RECIPE, and unlike python3-parse that is # not a decision anyone took: the recipe does not `inherit ptest' at all. The # case is here so a version bump that grows one is noticed -- at which point # the upstream tests, which currently ship in the RUNTIME package, would have # somewhere else to go. check_eq $P/no-ptest-package "" \ "$(sed -n '/^Package: python3-aniso8601-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If aniso8601 cannot be imported # then every probe case below reds at once and each names a different symptom; # this case is what says the cause is the import. It is also the cheapest # possible statement of the deliverable -- and, given the header, the one that # would have failed on an image without python3-datetime. check $P/import python3 -c 'import aniso8601' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode: the package ships precompiled .pyc files and OE writes them # with CHECKED-HASH invalidation, so each source is hashed and never # recompiled. A red here means a shipped .pyc was REJECTED -- a stale hash, a # python3 minor bump that changed the magic, or a missing __pycache__ -- and the # tree is being compiled from source on every import. # # Nothing in aniso8601 has import-time output of its own: no logging # configuration, no warning, no print. check_eq $P/import-quiet "" \ "$(python3 -c 'import aniso8601' 2>&1)" # THE UPSTREAM TESTS ARE NOT ON THE IMPORT PATH -- see the header. They ship in # the runtime package, so this is what says their cost is disk and nothing # else: `import aniso8601' does not reach them, so they add no runtime # dependency and cannot fail an import. check_eq $P/tests-not-imported "" \ "$(python3 -c 'import aniso8601, sys; print(",".join(m for m in sys.modules if "tests" in m))' 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 interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `dur_year_days' # distinguishable from `dur_year_seconds', and `res_date_week' from # `res_date_weekday' -- the pairs where one name is a prefix of another. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "196" "$(pv emitted_count)" # The tree answered out of the files this image installed, not from a stray copy # on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME aniso8601. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # ...and it is a PACKAGE. See the header: a release that flattened to one # module would still import and still pass every behavioural case below. check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION, FROM BOTH PLACES, AND THEN AGAINST EACH OTHER. check_eq $P/version-attr "9.0.1" "$(pv version_attr)" check_eq $P/dist-version "9.0.1" "$(pv dist_version)" check_eq $P/version-agrees "$(pv version_attr)" "$(pv dist_version)" # ------------------------------------------------------------------ the API --- # THE TWELVE PUBLIC FUNCTIONS, each reachable from the package root. Six parse # and six resolution, which is the whole documented surface. check_eq $P/api-parse-date "yes" "$(pv has_parse_date)" check_eq $P/api-parse-time "yes" "$(pv has_parse_time)" check_eq $P/api-parse-datetime "yes" "$(pv has_parse_datetime)" check_eq $P/api-parse-duration "yes" "$(pv has_parse_duration)" check_eq $P/api-parse-interval "yes" "$(pv has_parse_interval)" check_eq $P/api-parse-repeating "yes" "$(pv has_parse_repeating_interval)" check_eq $P/api-res-date "yes" "$(pv has_get_date_resolution)" check_eq $P/api-res-time "yes" "$(pv has_get_time_resolution)" check_eq $P/api-res-datetime "yes" "$(pv has_get_datetime_resolution)" check_eq $P/api-res-duration "yes" "$(pv has_get_duration_resolution)" check_eq $P/api-res-interval "yes" "$(pv has_get_interval_resolution)" check_eq $P/api-res-repeating "yes" "$(pv has_get_repeating_interval_resolution)" # THERE IS NO `__all__'. `from aniso8601 import *' therefore takes the # submodule names with it, which is worth pinning: a release that ADDED one # would narrow a caller's star-import without warning. check_eq $P/api-no-all "no" "$(pv has_all_attr)" # THE SUBMODULES, which are where `parse_timezone' and the exception classes # live -- neither is re-exported at package level, so a caller reaches for # `aniso8601.exceptions' by name. check_eq $P/api-submodules \ "builders,compat,date,decimalfraction,duration,exceptions,interval,resolution,time,timezone,utcoffset" \ "$(pv submodules)" # ------------------------------------------------------- the exception tree --- # THE SHAPE, ASSERTED DIRECTLY -- see the header. These four cases are the # port's most useful single finding and they are deliberately not left implicit # in the `err-*' group below. check_eq $P/isoformat-is-valueerror "yes" "$(pv isoformat_is_valueerror)" check_eq $P/rangecheck-is-valueerror "yes" "$(pv rangecheck_is_valueerror)" # `ISOFormatError' HAS NO SUBCLASSES. The ticket's phrasing assumes it does. check_eq $P/isoformat-subclasses "" "$(pv isoformat_subclasses)" # ...and the nine range errors are on the OTHER branch, so `except # ISOFormatError' does not catch one. check_eq $P/range-under-isoformat "no" "$(pv range_under_isoformat)" check_eq $P/rangecheck-subclasses \ "DayOutOfBoundsError,HoursOutOfBoundsError,LeapSecondError,MidnightBoundsError,MinutesOutOfBoundsError,MonthOutOfBoundsError,SecondsOutOfBoundsError,WeekOutOfBoundsError,YearOutOfBoundsError" \ "$(pv rangecheck_subclasses)" # ------------------------------------------------------------- parse_date --- # THE CALENDAR FORMS, extended and basic, which are the two spellings of the # same date and must agree. check_eq $P/date-extended "1984-05-07" "$(pv date_extended)" check_eq $P/date-basic "1984-05-07" "$(pv date_basic)" # REDUCED PRECISION DEFAULTS THE MISSING COMPONENTS TO 1 rather than refusing. # `1984-05' is the first of May and `1984' is New Year's Day -- which is what a # caller who wanted "just the year" has to know. check_eq $P/date-month-only "1984-05-01" "$(pv date_month_only)" check_eq $P/date-year-only "1984-01-01" "$(pv date_year_only)" # A TRUNCATED YEAR IS LEFT-JUSTIFIED WITH ZEROS, not centred on a century: `19' # is 1900, not 2019 and not 1919. `year_range_check' pads on the RIGHT # (`valuestr.ljust(4, "0")'), which is the surprising half. check_eq $P/date-trunc-year "1900-01-01" "$(pv date_trunc_year)" # THE ORDINAL FORM, YYYY-DDD -- the first of the three the ticket names, and # one the calendar forms cannot express. check_eq $P/date-ordinal "1981-04-05" "$(pv date_ordinal)" check_eq $P/date-ordinal-basic "1981-04-05" "$(pv date_ordinal_basic)" # DAY 366 IS A STATEMENT ABOUT THE YEAR, NOT ABOUT THE FORMAT: it parses in a # leap year and is a DayOutOfBoundsError in any other. Both directions, because # either one alone would pass on an implementation that ignored leap years. check_eq $P/date-ordinal-leap "2000-12-31" "$(pv date_ordinal_leap)" check_eq $P/date-ordinal-nonleap "DayOutOfBoundsError" "$(pv date_ordinal_nonleap)" # THE WEEK FORMS, AND THE WEEK-NUMBERING YEAR -- see the header. Four cases, # chosen so the year in the string differs from the year on the calendar in # both directions and so the "week 1 starts on the 1st" reading fails. check_eq $P/date-week-start "2004-12-27" "$(pv date_week_start)" check_eq $P/date-weekday "2005-01-01" "$(pv date_weekday)" check_eq $P/date-weekday-basic "2005-01-01" "$(pv date_weekday_basic)" # 2004-W01-1 is in 2003; 2009-W53-7 is in 2010; 2010-W01-1 is the 4th. check_eq $P/date-week-back "2003-12-29" "$(pv date_week_back)" check_eq $P/date-week-fwd "2010-01-03" "$(pv date_week_fwd)" check_eq $P/date-week-2010 "2010-01-04" "$(pv date_week_2010)" # ...and the answer round-trips through the INTERPRETER's own ISO calendar, # which is the strongest available statement that the week arithmetic is right # rather than merely stable. The expectation is the input string, so this case # asserts an agreement rather than a literal somebody typed. check_eq $P/week-agrees-isocalendar "2004-W53-6" "$(pv date_week_isocalendar)" # THE RESOLUTION ENUM, which is how a caller learns WHICH form it was handed. # The six values AND the six names: an enum whose members were reordered would # keep every parse_date case above green and silently change what a caller's # `== DateResolution.Week' means. check_eq $P/res-date-year "0" "$(pv res_date_year)" check_eq $P/res-date-month "1" "$(pv res_date_month)" check_eq $P/res-date-week "2" "$(pv res_date_week)" check_eq $P/res-date-weekday "3" "$(pv res_date_weekday)" check_eq $P/res-date-day "4" "$(pv res_date_day)" check_eq $P/res-date-ordinal "5" "$(pv res_date_ordinal)" check_eq $P/res-date-names \ "Year=0,Month=1,Week=2,Weekday=3,Day=4,Ordinal=5" "$(pv res_date_names)" # ------------------------------------------------------------- parse_time --- # THE FOUR PRECISIONS, extended and basic. `hh' alone is a legal time. check_eq $P/time-hms "11:31:14" "$(pv time_hms)" check_eq $P/time-hms-basic "11:31:14" "$(pv time_hms_basic)" check_eq $P/time-hm "11:31:00" "$(pv time_hm)" check_eq $P/time-h "11:00:00" "$(pv time_h)" # THE FRACTIONAL COMPONENT, which may appear on the LOWEST-ORDER element only # and is carried DOWNWARD into the fields below it -- a fractional hour becomes # minutes and a fractional minute becomes seconds, because `datetime.time' has # no fractional hour to put it in. Three cases for three arms of # `_distribute_microseconds'. check_eq $P/time-frac-sec "11:31:14.500000" "$(pv time_frac_sec)" check_eq $P/time-frac-min "11:31:30" "$(pv time_frac_min)" check_eq $P/time-frac-hour "11:30:00" "$(pv time_frac_hour)" # THE COMMA IS THE OTHER DECIMAL SEPARATOR, and ISO 8601 prefers it. check_eq $P/time-comma-frac "11:31:14.500000" "$(pv time_comma_frac)" # THE UTC OFFSETS, in all four spellings. Reported through `isoformat()', which # renders the offset, so each is a statement about the tzinfo and not only about # the wall-clock fields. check_eq $P/time-utc-z "11:31:14+00:00" "$(pv time_utc_z)" check_eq $P/time-off-ext "11:31:14+04:00" "$(pv time_off_ext)" check_eq $P/time-off-basic "11:31:14-05:30" "$(pv time_off_basic)" check_eq $P/time-off-hour "11:31:14+04:00" "$(pv time_off_hour)" # THE tzinfo IS THIS LIBRARY'S OWN `UTCOffset', not `datetime.timezone' -- so a # caller comparing `tzinfo is datetime.timezone.utc' is wrong even for `Z'. check_eq $P/time-tz-class "UTCOffset" "$(pv time_tz_class)" # ...and `Z' is NAMED "UTC" rather than "Z". check_eq $P/time-tz-name "UTC" "$(pv time_tz_name)" check_eq $P/time-tz-offset-s "14400" "$(pv time_tz_offset_s)" # `dst()' is always zero by design -- ISO 8601 carries offsets, not daylight # rules, so a caller must not read a summer offset as a DST flag. check_eq $P/time-tz-dst-s "0" "$(pv time_tz_dst_s)" # HOUR 24 IS MIDNIGHT AND IS MOVED TO 0, so `24:00:00' does NOT round-trip as # the string it came from -- and anything else at hour 24 is a # MidnightBoundsError rather than an hours range error. check_eq $P/time-midnight "00:00:00" "$(pv time_midnight)" check_eq $P/time-midnight-bad "MidnightBoundsError" "$(pv time_midnight_bad)" # A LEAP SECOND IS REFUSED, and refused DELIBERATELY: the seconds limit admits # 60 ("with 60 representing a leap second") and the builder raises anyway. So # this is not a range that stops at 59, and the class says which. check_eq $P/time-leap-second "LeapSecondError" "$(pv time_leap_second)" check_eq $P/res-time-sec "0" "$(pv res_time_sec)" check_eq $P/res-time-min "1" "$(pv res_time_min)" check_eq $P/res-time-hour "2" "$(pv res_time_hour)" # --------------------------------------------------------- parse_datetime --- check_eq $P/dt-plain "1981-04-05T23:21:28.512400" "$(pv dt_plain)" check_eq $P/dt-utc "1981-04-05T23:21:28.512400+00:00" "$(pv dt_utc)" check_eq $P/dt-offset "1981-04-05T23:21:28.512400+11:15" "$(pv dt_offset)" # THE DELIMITER IS A PARAMETER, not a constant: a space is the other spelling in # the wild and is accepted only when asked for. Both halves, because the # default's refusal is what a caller meets first. check_eq $P/dt-space "1981-04-05T23:21:28" "$(pv dt_space)" check_eq $P/dt-space-default "ISOFormatError" "$(pv dt_space_default)" # THE DATE HALF TAKES ANY DATE FORM, so the ordinal and week forms compose with # a time. This is the case that says parse_datetime DELEGATES rather than # carrying a second date parser that could drift from the first. check_eq $P/dt-ordinal "1981-04-05T23:21:28" "$(pv dt_ordinal)" check_eq $P/dt-week "2005-01-01T23:21:28" "$(pv dt_week)" check_eq $P/res-datetime "0" "$(pv res_datetime)" # --------------------------------------------------------- parse_duration --- # THE DESIGNATOR FORM, the ticket's own fixture, compared FIELD BY FIELD. A # timedelta repr suppresses whichever of the three fields is zero, so `P1Y' and # `P365D' have the same repr -- comparing reprs would not distinguish a release # that changed DAYS_PER_YEAR from one that changed nothing. check_eq $P/dur-full-days "1279" "$(pv dur_full_days)" check_eq $P/dur-full-seconds "45005" "$(pv dur_full_seconds)" check_eq $P/dur-full-micro "0" "$(pv dur_full_micro)" # THE COMBINED DATE-AND-TIME FORM, PT