. "$BST_LIB" # BST-TIMEOUT: 1200 # FOUR 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 four. # # # # They are a bare `import', a second one whose STDERR is # # the assertion (`import-quiet'), a third that asserts the # # upstream tests are NOT in site-packages, and the probe -- # # which takes ONE start for a hundred and eighty-eight # # measurements. The work inside the probe is calendar # # arithmetic and string formatting over fixed 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 300s per start, the same budget # # 99-python3-aniso8601 and 99-python3-parse set for their # # three, and roughly five times the ~60s a python3 start # # has measured at here. # # # # A suite-timeout red is a batch-wide cost, so if the four # # 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-arrow 1.3.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-arrow_1.3.0.bb, `inherit pypi # python_flit_core ptest', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # ONE type -- `Arrow' -- that is a timezone-aware instant, a parser, a # formatter and a calendar-aware arithmetic in a single object, offered as a # replacement for juggling `datetime', `dateutil' and `pytz' by hand. Three # module-level entry points (`get', `now', `utcnow'), sixteen named format # constants, and about eighty locales for `humanize'. # # It is eleven pure-Python files and a `py.typed' marker. There is no C, no # configure and no compile step, which is also what makes it portable here: # this image's python3 is ONE STATIC BINARY with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a module carrying a compiled extension # could not be imported whatever it built to. The `pkg-no-so' and # `pkg-only-python' cases assert that from the packaged file list, and # `pulled-nonstdlib' asserts it again from the import's own measured closure. # # ============================================================================ # THE CLOCK IS AN INPUT HERE, NEVER AN AMBIENT FACT. # ============================================================================ # # Every instant this suite measures is a literal in the probe or derived from # one. `utcnow()' and `now()' are exercised, but only for their TYPE and their # tzinfo -- never for a value -- so no case below depends on when the suite # runs, on the guest's timezone, or on any time source off the machine. That # matters more than usual here: this guest boots with no RTC and no NTP, so a # suite that asserted anything about the current instant would be asserting # about the platform rather than about arrow. # # `humanize' is the place this could quietly go wrong, and it is why every # `hum-*' case passes an explicit `other': with the argument omitted the answer # is scored against `utcnow()' and has no fixed value at all. # # ============================================================================ # A CALENDAR SHIFT IS NOT ARITHMETIC, AND THAT IS THE HEART OF THE SUITE. # ============================================================================ # # `shift()' is a `dateutil.relativedelta', so three separate things are true # that a caller reading `+ timedelta(...)' does not expect, and each has its # own group below: # # - MONTH-END IS CLAMPED. 31 January + 1 month is 28 February, and the shift # is therefore NOT INVERTIBLE: +1 month then -1 month is 28 January. A # round trip through a month shift loses days and nothing raises. # # - A DAY SHIFT PRESERVES THE WALL CLOCK, NOT THE ELAPSED TIME. Across the US # Pacific spring-forward, noon to noon is 23 hours. # # - AN HOUR SHIFT PRESERVES THE ELAPSED TIME, NOT THE WALL CLOCK. 01:30 + 1 # hour is 03:30 that same morning, because 02:00 does not exist. # # `arith-add-timedelta' sits at the end as the control: `+ timedelta(days=1)' # IS +86400 seconds, whatever the calendar is doing. # # ============================================================================ # TWO THINGS arrow ACCEPTS THAT IT ARGUABLY SHOULD NOT, AND ONE IT REFUSES. # ============================================================================ # # `+9999' AS AN OFFSET IS ACCEPTED. It is 99 hours and 99 minutes; the parser # reads the four digits as HHMM with no bound and `get()' reports success. The # object is poisoned rather than odd: `utcoffset()' -- which `isoformat()', # `format()' and every comparison call -- raises `ValueError' out of the # standard library, arbitrarily far from the `get()' that produced it. # # A FORMAT STRING SHORTER THAN ITS INPUT IS ACCEPTED. The tokenised parser # matches a prefix and discards the rest, so `"2013-05-05 12:30:45"' read with # `"YYYY-MM-DD"' is midnight and the time is gone, silently. # # A LOWER-CASE `z' IS REFUSED. RFC 3339 §5.6 makes the letters # case-insensitive; arrow's regex does not. A producer that lower-cases its # timestamps is unreadable here. # # None of the three is a brintOS defect -- all three reproduce on an x86-64 # control -- so none is filed. They are asserted because they are the shapes a # caller on this image will meet. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was MEASURED on an x86-64 control -- CPython 3.12.3 # against the image's 3.12, python-dateutil 2.8.2 against the image's 2.9.0 -- # by running data/python3-arrow-probe.py against the tree unpacked from the # built .deb, and the suite was then run END TO END against that tree with its # site-packages, dpkg status and dpkg file-list paths redirected at it. None of # them is a prediction. # # The offsets asserted here are NOT properties of tzdata alone, and the note # that used to stand here saying they were is why 24 of them red on this image. # `dateutil.tz.gettz' reads the system zoneinfo on both sides, but its `tzfile' # parses ONLY the 32-bit TZif v1 block (`struct.unpack(">6l", ...)' in # `dateutil/tz/tz.py:_read_tzfile'), and this image's tzdata is a SLIM build # whose v1 block is empty -- measured on the staged rootfs, every zone file # carries `timecnt=0, typecnt=1, charcnt=1'. `gettz' therefore returns a # non-None `tzfile' with no transitions and a nameless type, so every named # zone answers +00:00 with an empty `tzname()'. The stdlib `zoneinfo' reads # the 64-bit v2 block off the SAME byte-identical file and answers -07:00/PDT. # See the test plan's BRINTOS-2347 section: these reds are a property of the # image's tzdata build variant, not of arrow and not of this recipe. # # ============================================================================ # UPSTREAM'S OWN TESTS ARE NOT ON THIS IMAGE, AND NOT BECAUSE OF ANYTHING HERE. # ============================================================================ # # The recipe does `inherit ptest' and stages `tests/*', but this distro's # DISTRO_FEATURES is "ipv4 x11 wayland" with no `ptest' in it, so # `ptest.bbclass' never adds `${PN}-ptest' to PACKAGES and no such .deb is # produced. `pkg-no-ptest' asserts that rather than leaving it to be # discovered. Forcing it on would put python3-pytest, python3-pytest-mock and # python3-unittest-automake-output on the image, none of which is ported, and # pytest's own RDEPENDS name several more. # # THE python3-simpleeval ROUTE IS CLOSED TOO, which is worth saying because it # is the obvious next idea: there, upstream's tests were `unittest.TestCase' # methods and pytest was only ever the runner, so the file could ship as suite # data. arrow's 645 test functions are pytest-NATIVE -- 89 # `@pytest.mark.usefixtures' decorations against twelve class-scoped fixtures # in `conftest.py', 43 uses of pytest-mock's `mocker', 170 `pytest.raises' # context managers. `python3 -m unittest' collects none of it. This suite # stands in their place. # # And unlike aniso8601 beside it, the upstream tests do not leak into # site-packages either: `[tool.flit.module] name = "arrow"' packages exactly # the one module directory, so `tests/' is not shipped by the runtime .deb. # `no-tests-in-site-packages' and `tests-not-importable' assert both halves. P=python3-arrow bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/arrow DI=$SITE/arrow-1.3.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-arrow.list PROBE=$BST_DATA/python3-arrow-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-arrow.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ what shipped --- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/arrow.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/arrow-py test -s "$PKGDIR/arrow.py" check $P/factory-py test -s "$PKGDIR/factory.py" check $P/formatter-py test -s "$PKGDIR/formatter.py" check $P/locales-py test -s "$PKGDIR/locales.py" check $P/parser-py test -s "$PKGDIR/parser.py" check $P/util-py test -s "$PKGDIR/util.py" check $P/constants-py test -s "$PKGDIR/constants.py" check $P/api-py test -s "$PKGDIR/api.py" check $P/version-py test -s "$PKGDIR/_version.py" # THE PEP 561 MARKER SHIPS. It is what makes a consumer's mypy read this # package's own annotations, and it is the only non-.py file in the module # directory -- which is why `pkg-only-python' below has to allow for it. check $P/py-typed test -f "$PKGDIR/py.typed" check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/no-cli test ! -e /usr/bin/arrow # UPSTREAM'S TESTS ARE NOT IN site-packages -- see the header. check $P/no-tests-in-site-packages test ! -d "$PKGDIR/tests" check $P/no-conftest test ! -e "$SITE/conftest.py" 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 SHIPPED FILE IS PYTHON, its bytecode, its dist-info or the py.typed # marker. A compiled extension could not be imported on this arm at all # (BRINTOS-1476), so this is the packaging-side statement of what # `pulled-nonstdlib' says from inside the interpreter. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.3.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: arrow" cat "$DI/METADATA" check_out $P/dist-info-summary "Summary: Better dates & times for Python" cat "$DI/METADATA" check $P/dist-info-licence test -s "$DI/LICENSE" # THE WHEEL WAS BUILT BY flit, not by setuptools -- `python_flit_core' is the # recipe's build class, and this is the case that says the class did its job. check_out $P/dist-info-generator "Generator: flit" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # ------------------------------------------------------------- the package --- sed -n '/^Package: python3-arrow$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" # THE WHOLE Depends LINE, BY NAME, IN BOTH DIRECTIONS. The bbappend removes # four entries meta-python declares and adds the two it omits; these eight # cases are what turn that file's reasoning into something a release can red. check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-dateutil "python3-dateutil" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" check_eq $P/pkg-no-depends-json "" \ "$(grep -o 'python3-json' "$W/dep" 2>/dev/null)" check_eq $P/pkg-no-depends-compression "" \ "$(grep -o 'python3-compression' "$W/dep" 2>/dev/null)" check_eq $P/pkg-no-depends-zoneinfo "" \ "$(grep -o 'python3-dateutil-zoneinfo' "$W/dep" 2>/dev/null)" check_eq $P/pkg-no-depends-types "" \ "$(grep -o 'python3-types-python-dateutil' "$W/dep" 2>/dev/null)" # ...AND THE TYPING-STUB PACKAGE IS NOT ON THE IMAGE AT ALL. It is # BRINTOS-2301's deliverable; the bbappend removes the RDEPENDS precisely so # that this port does not land it as a side effect, and this case is what would # notice if it did. check_eq $P/no-types-dateutil-package "" \ "$(sed -n '/^Package: python3-types-python-dateutil$/p' /var/lib/dpkg/status 2>/dev/null)" # NO -ptest PACKAGE IS BUILT -- see the header. check_eq $P/pkg-no-ptest "" \ "$(sed -n '/^Package: python3-arrow-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # ---------------------------------------------------------------- it imports --- check $P/import python3 -c 'import arrow' # ...AND SAYS NOTHING WHILE DOING IT. arrow imports `dateutil.rrule', which # reaches deep into `dateutil'; a DeprecationWarning from any of it would land # on a caller's stderr on every start. check_eq $P/import-quiet "" \ "$(python3 -c 'import arrow' 2>&1)" # UPSTREAM'S TESTS ARE NOT IMPORTABLE, which is the runtime half of # `no-tests-in-site-packages' above. check_eq $P/tests-not-importable "ModuleNotFoundError" \ "$(python3 -c 'try: import arrow.tests except ModuleNotFoundError: print("ModuleNotFoundError")' 2>&1)" 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 check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "190" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # THE MODULE THE PROBE MEASURED IS THE PACKAGED ONE. The probe sits in # ${libdir}/brintos-tests/data and the interpreter puts a script's own # directory first on sys.path, so this case is what would catch a probe that # had shadowed the module it measures. check_eq $P/module-file "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "1.3.0" "$(pv version_attr)" check_eq $P/dist-version "1.3.0" "$(pv dist_version)" # ------------------------------------------------------------------- API --- # `arrow/__init__.py' names seventeen exports and every one of them resolves -- # the module's own re-export list is asserted before anything below asserts # what any of them does. `__all__' exists because mypy `--no-implicit-reexport' # requires it, so a release that dropped a name from it would break a typed # caller's import without breaking ours. check_eq $P/all-count "17" "$(pv all_count)" check_eq $P/api-missing "" "$(pv api_missing)" check_eq $P/type-arrow "type" "$(pv type_arrow)" check_eq $P/get-is-callable "yes" "$(pv get_is_callable)" # ------------------------------------------------------- the ambient clock --- # THE ONLY THREE CASES IN THIS SUITE THAT TOUCH THE WALL CLOCK, and none of # them asserts a value: the TYPE, the tzinfo of `utcnow()' (UTC, always) and # the tzinfo of `now()'. `Asia/Kolkata' is the zone chosen for the third # because it has had no DST since 1945 and a fixed +05:30, so its offset is a # constant rather than a function of the date the suite happens to run on. check_eq $P/utcnow-type "Arrow" "$(pv utcnow_type)" check_eq $P/utcnow-offset "0" "$(pv utcnow_offset)" check_eq $P/utcnow-tzname "UTC" "$(pv utcnow_tzname)" check_eq $P/now-tz-offset "19800" "$(pv now_tz_offset)" # ---------------------------------------------------------- arrow.get: ISO --- # ONE INSTANT, DECOMPOSED. `isoformat()' carries both the wall clock and the # offset, so a release that dropped the offset while keeping the clock would # still print a plausible string; the offset, the epoch second and the # microsecond are therefore asserted separately, and the last case converts to # UTC so the two representations of one instant are both pinned. check_eq $P/iso-offset-isoformat "2013-05-11T21:23:58.970460+07:00" "$(pv iso_offset_isoformat)" check_eq $P/iso-offset-utcoffset "25200" "$(pv iso_offset_utcoffset)" check_eq $P/iso-offset-timestamp "1368282238" "$(pv iso_offset_timestamp)" check_eq $P/iso-offset-microsecond "970460" "$(pv iso_offset_microsecond)" check_eq $P/iso-offset-utc-wall "2013-05-11 14:23:58" "$(pv iso_offset_utc_wall)" # ------------------------------------------------------- arrow.get: RFC 3339 --- # THE `Z' IS UPPER CASE ONLY. RFC 3339 §5.6 says the letters are # case-insensitive; arrow's regex does not, and `2013-05-05t12:30:45z' is a # ParserError. A producer that lower-cases its timestamps -- and they exist -- # is not readable here, which is worth a case rather than a discovery. check_eq $P/rfc3339-z "2013-05-05T12:30:45+00:00" "$(pv rfc3339_z)" check_eq $P/rfc3339-z-offset "0" "$(pv rfc3339_z_offset)" check_eq $P/rfc3339-lower-z "" "$(pv rfc3339_lower_z)" check_eq $P/rfc3339-space "2013-05-05T12:30:45+00:00" "$(pv rfc3339_space)" # A NAKED DATE IS MIDNIGHT **UTC**, NOT MIDNIGHT LOCAL. A caller who reads it # as local is wrong by the offset, silently, on every date it parses. The # three shortened calendar forms below default the missing fields to the start # of their period rather than refusing. check_eq $P/bare-date "2013-05-05T00:00:00+00:00" "$(pv bare_date)" check_eq $P/bare-date-offset "0" "$(pv bare_date_offset)" check_eq $P/basic-no-dashes "2013-05-05T00:00:00+00:00" "$(pv basic_no_dashes)" check_eq $P/year-month "2013-05-01T00:00:00+00:00" "$(pv year_month)" check_eq $P/year-only "2013-01-01T00:00:00+00:00" "$(pv year_only)" # A SUB-MICROSECOND FRACTION IS ROUNDED, NOT TRUNCATED AND NOT REFUSED: # `.123456789' comes back as 123457 microseconds. A caller who stores # nanoseconds and reads them back has lost the last three digits AND had the # sixth changed under them. check_eq $P/nanoseconds-truncated "123457" "$(pv nanoseconds_truncated)" # THE THREE OFFSET SPELLINGS ISO 8601 ALLOWS, all accepted. `-00:00' is the # one worth a case: RFC 3339 gives it a distinct meaning ("offset unknown"), # and here it is simply zero. check_eq $P/offset-no-colon "25200" "$(pv offset_no_colon)" check_eq $P/offset-hours-only "25200" "$(pv offset_hours_only)" check_eq $P/offset-negative-zero "0" "$(pv offset_negative_zero)" # --------------------------------------------------------- arrow.get: epoch --- # AN EPOCH IS A NUMBER, AND ONLY A NUMBER. arrow 0.x parsed the STRING # "1367900664" as a timestamp; 1.0 removed that deliberately, because the same # string is also a plausible malformed date. A caller who upgraded and kept # passing the string gets a ParserError rather than an instant -- and the # documented way to mean it anyway is the `X' token. check_eq $P/epoch-int "2013-05-07T04:24:24+00:00" "$(pv epoch_int)" check_eq $P/epoch-float "2013-05-07T04:24:24.152325+00:00" "$(pv epoch_float)" check_eq $P/epoch-int-offset "0" "$(pv epoch_int_offset)" check_eq $P/epoch-string-raises "ParserError" "$(pv epoch_string_raises)" check_eq $P/epoch-string-via-token "2013-05-07T04:24:24+00:00" "$(pv epoch_string_via_token)" # ------------------------------------------------ arrow.get: a format string --- # A LIST OF FORMATS IS TRIED IN ORDER and the first that matches wins, which is # why `05-05-2013' against `["YYYY-MM-DD", "MM-DD-YYYY"]' is the 5th of May and # not a refusal -- the first pattern matched. check_eq $P/tokens-parse "2013-05-05T12:30:45+00:00" "$(pv tokens_parse)" # A FORMAT SHORTER THAN THE INPUT IS **NOT** AN ERROR. The tokenised parser # matches a PREFIX and drops the remainder, so `"2013-05-05 12:30:45"' read # with `"YYYY-MM-DD"' is midnight on the 5th and the time is thrown away with # nothing raised. Both halves are asserted, because either alone reads as the # other's absence. check_eq $P/tokens-short-format-raises "" "$(pv tokens_short_format_raises)" check_eq $P/tokens-short-format-value "2013-05-05T00:00:00+00:00" "$(pv tokens_short_format_value)" check_eq $P/tokens-list "2013-05-05T00:00:00+00:00" "$(pv tokens_list)" # ------------------------------------------------ arrow.get: the other inputs --- # A NAIVE `datetime' IS ASSUMED UTC unless a zone is passed beside it, and the # zone argument LOCALISES rather than converts: the wall clock is kept and the # offset is attached. `arrow.get()' with no argument at all is `utcnow()', so # only its tzinfo is assertable. check_eq $P/from-naive-datetime "2013-05-05T12:30:45+00:00" "$(pv from_naive_datetime)" check_eq $P/from-naive-datetime-tz "2013-05-05T12:30:45-07:00" "$(pv from_naive_datetime_tz)" check_eq $P/from-date "2013-05-05T00:00:00+00:00" "$(pv from_date)" check_eq $P/from-parts "2013-05-05T12:30:45+00:00" "$(pv from_parts)" check_eq $P/from-arrow "2013-05-05T12:30:45+00:00" "$(pv from_arrow)" check_eq $P/get-noargs-offset "0" "$(pv get_noargs_offset)" # ------------------------------------------------------------ the refusals --- # ASSERTED BY EXCEPTION TYPE, NEVER BY MESSAGE. The messages are English prose # and a release may reword any of them; the TYPE is what a caller's `except' # clause names. `ParserError' is re-exported from the package root and is a # `ValueError' subclass, which together are what make `except ValueError' a # working -- if over-broad -- spelling. # # THE TREE HAS TWO BRANCHES AND THEY ARE NOT THE SAME CLASS. A string the # GRAMMAR rejects is arrow's own `ParserError'; a string that parses into # out-of-range FIELDS is a bare `ValueError' raised by `datetime' itself, one # layer below arrow. A caller catching `arrow.ParserError' alone catches the # first group and not the second -- `2013-13-05' escapes it -- which is the # distinction these ten cases exist to pin. check_eq $P/parsererror-at-root "yes" "$(pv parsererror_at_root)" check_eq $P/parsererror-is-valueerror "yes" "$(pv parsererror_is_valueerror)" check_eq $P/err-garbage "ParserError" "$(pv err_garbage)" check_eq $P/err-empty "ParserError" "$(pv err_empty)" check_eq $P/err-month-13 "ValueError" "$(pv err_month_13)" check_eq $P/err-day-32 "ValueError" "$(pv err_day_32)" check_eq $P/err-feb-30 "ValueError" "$(pv err_feb_30)" check_eq $P/err-not-leap "ValueError" "$(pv err_not_leap)" check_eq $P/err-hour-25 "ValueError" "$(pv err_hour_25)" check_eq $P/err-trailing-garbage "ParserError" "$(pv err_trailing_garbage)" # AND ONE INPUT IS ACCEPTED THAT SHOULD NOT BE. `+9999' is 99 hours and 99 # minutes; the parser reads the four digits as HHMM and hands them to # `timedelta' with no bound, so `get()' REPORTS SUCCESS and returns an `Arrow'. # The object is poisoned: `utcoffset()' -- which `isoformat()', `format()' and # every comparison call -- then raises `ValueError' out of the standard # library, arbitrarily far from the `get()' that produced it. Both facts are # asserted; either alone reads as the other's absence. check_eq $P/err-bad-offset "" "$(pv err_bad_offset)" check_eq $P/bad-offset-type "Arrow" "$(pv bad_offset_type)" check_eq $P/bad-offset-seconds "" "$(pv bad_offset_seconds)" check_eq $P/err-unknown-zone "ParserError" "$(pv err_unknown_zone)" check_eq $P/err-nonsense-type "TypeError" "$(pv err_nonsense_type)" # ============================================================================= # shift: A CALENDAR SHIFT IS A `relativedelta', NOT A `timedelta'. # ============================================================================= # # MONTH-END IS CLAMPED, SILENTLY. 31 January plus one month is 28 February -- # not 3 March, and not an error. In a leap year the same shift lands on the # 29th, which is what makes the pair a statement about the calendar rather than # about a constant. check_eq $P/shift-month-end "2013-02-28" "$(pv shift_month_end)" check_eq $P/shift-month-end-leap "2012-02-29" "$(pv shift_month_end_leap)" # ...SO THE SHIFT IS NOT INVERTIBLE. Plus one month then minus one month is # the 28th of January, not the 31st. A caller who round-trips a date through a # month shift loses days and nothing raises, which is the single most expensive # thing in this file to not know. check_eq $P/shift-month-roundtrip "2013-01-28" "$(pv shift_month_roundtrip)" check_eq $P/shift-month-roundtrip-is-identity "no" "$(pv shift_month_roundtrip_is_identity)" # THE LEAP DAY HAS THE SAME SHAPE: 29 February plus one year is the 28th, plus # four years is the 29th again. The last two cases are the century rule -- # 1900 was not a leap year and 2000 was -- so a release that reimplemented the # calendar is named here rather than only causing a spray of reds. check_eq $P/shift-leap-year "2021-02-28" "$(pv shift_leap_year)" check_eq $P/shift-leap-four-years "2024-02-29" "$(pv shift_leap_four_years)" check_eq $P/shift-leap-1900 "1900-02-28" "$(pv shift_leap_1900)" check_eq $P/shift-leap-2000 "2000-02-29" "$(pv shift_leap_2000)" # ============================================================================= # shift ACROSS A DST TRANSITION: THE WALL CLOCK IS PRESERVED, THE ELAPSED TIME # IS NOT. # ============================================================================= # # US Pacific springs forward at 02:00 local on 2023-03-12. Shifting one DAY # from noon on the 11th lands on noon on the 12th -- the same wall clock, a # different offset, and 82800 seconds (23 hours) of real time. A caller who # read `shift(days=+1)' as `+86400s' is an hour out, twice a year, in one # direction each time. check_eq $P/dst-day-before "2023-03-11 12:00:00 -08:00" "$(pv dst_day_before)" check_eq $P/dst-day-after "2023-03-12 12:00:00 -07:00" "$(pv dst_day_after)" check_eq $P/dst-day-offset-before "-28800" "$(pv dst_day_offset_before)" check_eq $P/dst-day-offset-after "-25200" "$(pv dst_day_offset_after)" check_eq $P/dst-day-elapsed-seconds "82800" "$(pv dst_day_elapsed_seconds)" # THE HOUR SHIFT IS THE OTHER HALF OF THE SAME STATEMENT, and it points the # other way: 01:30 plus one hour is 03:30, because 02:00 does not exist that # morning. The INSTANT moved by exactly 3600 seconds while the wall clock # moved by two hours. check_eq $P/dst-gap-before "2023-03-12 01:30:00 -08:00" "$(pv dst_gap_before)" check_eq $P/dst-gap-after "2023-03-12 03:30:00 -07:00" "$(pv dst_gap_after)" check_eq $P/dst-gap-elapsed-seconds "3600" "$(pv dst_gap_elapsed_seconds)" # THE AUTUMN REPEAT. 01:30 happens TWICE on 2023-11-05 in US Pacific, an hour # apart. arrow reports both, with the same wall clock and different offsets, # rather than collapsing them -- so a log written in local time that morning # has an hour of entries no reader can order, and these three cases are what # say arrow is not the thing that lost the distinction. check_eq $P/dst-fold-first "2023-11-05 01:30:00 -07:00" "$(pv dst_fold_first)" check_eq $P/dst-fold-second "2023-11-05 01:30:00 -08:00" "$(pv dst_fold_second)" check_eq $P/dst-fold-same-wall "yes" "$(pv dst_fold_same_wall)" check_eq $P/dst-fold-offsets "-25200,-28800" "$(pv dst_fold_offsets)" # THE PLAIN SHIFTS, so the group above reads as the exception it is. check_eq $P/shift-days "2013-05-08T12:30:45+00:00" "$(pv shift_days)" check_eq $P/shift-weeks "2013-05-19T12:30:45+00:00" "$(pv shift_weeks)" check_eq $P/shift-hours "2013-05-05T09:30:45+00:00" "$(pv shift_hours)" check_eq $P/shift-microseconds "2013-05-05T12:30:45.000001+00:00" "$(pv shift_microseconds)" # `weekday=' IS NOT A SHIFT AT ALL. It moves FORWARD to the next named weekday # -- Monday from a Sunday is the next day -- and STANDS STILL when the date is # already on that weekday, which is the boundary a caller iterating over weeks # meets first. check_eq $P/shift-weekday-next "2013-05-06 Mon" "$(pv shift_weekday_next)" check_eq $P/shift-weekday-same "2013-05-05 Sun" "$(pv shift_weekday_same)" # `replace()' IS THE OTHER OPERATION AND IS ABSOLUTE, not relative. It refuses # a PLURAL keyword, which is exactly the mistake a caller alternating between # the two methods makes: `replace(hours=6)' is a `ValueError', not six hours # later and not six o'clock. check_eq $P/replace-absolute "2013-05-05T06:30:45+00:00" "$(pv replace_absolute)" check_eq $P/replace-plural-raises "ValueError" "$(pv replace_plural_raises)" # ============================================================================= # format: ONE REFERENCE INSTANT, EVERY NAMED CONSTANT ENUMERATED. # ============================================================================= # # The instant is 2013-05-11T21:23:58.970460+07:00 throughout, so a release that # changed one template is named by its own case rather than by a spray. # `format()' with no argument is not `isoformat()' -- it drops the microseconds # and separates with a space -- and `str()' IS `isoformat()', which is a # distinction worth pinning because the two are one keystroke apart. check_eq $P/fmt-default "2013-05-11 21:23:58+07:00" "$(pv fmt_default)" check_eq $P/fmt-tokens "2013-05-11 21:23:58 +07:00" "$(pv fmt_tokens)" check_eq $P/fmt-offset-z "+0700" "$(pv fmt_offset_z)" check_eq $P/fmt-12-hour "2013-05-11 09:23:58 PM" "$(pv fmt_12_hour)" check_eq $P/fmt-day-names "Saturday 131 Sat May May" "$(pv fmt_day_names)" check_eq $P/fmt-escaped "today is 2013" "$(pv fmt_escaped)" check_eq $P/fmt-atom "2013-05-11 21:23:58+07:00" "$(pv fmt_atom)" # `FORMAT_COOKIE' AND `FORMAT_RFC850' END IN A SPACE, and it is a finding, not # a rendering artefact. Both templates end in `ZZZ' -- the timezone NAME -- # and a fixed-offset tzinfo parsed out of `+07:00' has none, so the token # renders empty and the separator before it survives. Neither string is a # valid Cookie or RFC 850 date as a result. The `|' is a terminator this file # appends so the trailing space is visible in the value at all; the third case # renders the same template for an instant that DOES carry a zone name, which # is what shows the emptiness to be about the tzinfo rather than the token. check_eq $P/fmt-cookie "Saturday, 11-May-2013 21:23:58 |" "$(pv fmt_cookie)" check_eq $P/fmt-rfc822 "Sat, 11 May 13 21:23:58 +0700" "$(pv fmt_rfc822)" check_eq $P/fmt-rfc850 "Saturday, 11-May-13 21:23:58 |" "$(pv fmt_rfc850)" check_eq $P/fmt-cookie-named-zone "Saturday, 11-May-2013 07:23:58 PDT|" "$(pv fmt_cookie_named_zone)" check_eq $P/fmt-rfc1036 "Sat, 11 May 13 21:23:58 +0700" "$(pv fmt_rfc1036)" check_eq $P/fmt-rfc1123 "Sat, 11 May 2013 21:23:58 +0700" "$(pv fmt_rfc1123)" check_eq $P/fmt-rfc2822 "Sat, 11 May 2013 21:23:58 +0700" "$(pv fmt_rfc2822)" check_eq $P/fmt-rfc3339 "2013-05-11 21:23:58+07:00" "$(pv fmt_rfc3339)" check_eq $P/fmt-rss "Sat, 11 May 2013 21:23:58 +0700" "$(pv fmt_rss)" check_eq $P/fmt-w3c "2013-05-11 21:23:58+07:00" "$(pv fmt_w3c)" check_eq $P/str-is-isoformat "yes" "$(pv str_is_isoformat)" # A FORMAT ROUND-TRIPS THROUGH THE PARSER for the two constants a wire protocol # actually carries -- and loses the microseconds on the way, because neither # template has a sub-second token. `FORMAT_RFC3339' is also worth reading # twice: it separates with a SPACE, not with `T', so it is not the RFC 3339 # spelling most producers emit. check_eq $P/roundtrip-rfc3339 "2013-05-11T21:23:58+07:00" "$(pv roundtrip_rfc3339)" check_eq $P/roundtrip-rfc2822 "2013-05-11T21:23:58+07:00" "$(pv roundtrip_rfc2822)" # ============================================================================= # humanize: ALWAYS AGAINST AN EXPLICIT `other'. # ============================================================================= # # With the argument omitted the answer is scored against `utcnow()' and has no # fixed value, which is precisely why the ticket's "the clock is an input" # requirement is met by passing one. `other' LATER than the instant reads # "ago" and `other' earlier reads "in", which is the direction a caller gets # backwards first. # # THE GRANULARITY BOUNDARIES ARE THE POINT. Five seconds is "just now", 45 # minutes is minutes, five hours is hours, one of anything is the indefinite # article -- "a day ago", not "1 day ago". A release that re-tuned one # threshold moves exactly one of these cases. check_eq $P/hum-hour-ago "an hour ago" "$(pv hum_hour_ago)" check_eq $P/hum-hour-hence "in an hour" "$(pv hum_hour_hence)" check_eq $P/hum-seconds "just now" "$(pv hum_seconds)" check_eq $P/hum-minutes "45 minutes ago" "$(pv hum_minutes)" check_eq $P/hum-hours "5 hours ago" "$(pv hum_hours)" check_eq $P/hum-day "a day ago" "$(pv hum_day)" check_eq $P/hum-days "3 days ago" "$(pv hum_days)" check_eq $P/hum-week "a week ago" "$(pv hum_week)" check_eq $P/hum-month "a month ago" "$(pv hum_month)" check_eq $P/hum-months "5 months ago" "$(pv hum_months)" check_eq $P/hum-year "a year ago" "$(pv hum_year)" check_eq $P/hum-years "5 years ago" "$(pv hum_years)" check_eq $P/hum-now "just now" "$(pv hum_now)" check_eq $P/hum-granularity-hour "5 hours ago" "$(pv hum_granularity_hour)" check_eq $P/hum-granularity-list "2 days and 3 hours ago" "$(pv hum_granularity_list)" check_eq $P/hum-only-distance "5 hours" "$(pv hum_only_distance)" check_eq $P/hum-locale-fr "il y a 5 heures" "$(pv hum_locale_fr)" check_eq $P/hum-locale-unknown-raises "ValueError" "$(pv hum_locale_unknown_raises)" check_eq $P/dehumanize "2013-05-05T17:30:45+00:00" "$(pv dehumanize)" check_eq $P/dehumanize-unparseable-raises "ValueError" "$(pv dehumanize_unparseable_raises)" # THE LOCALE REGISTRY IS ASSERTED BY NAME, NOT BY RENDERING. `arrow.locales' # carries scores of locales and most of them produce non-ASCII; the guest runs # under the C locale, so whether CPython coerces stdout to UTF-8 is a property # of the platform rather than of this package. `fr-fr' is exercised above # because every string it produces for these inputs is ASCII; the rest are # counted and looked up by key. check_eq $P/locale-count "187" "$(pv locale_count)" check_eq $P/locale-has-en-us "yes" "$(pv locale_has_en_us)" check_eq $P/locale-has-fr-fr "yes" "$(pv locale_has_fr_fr)" check_eq $P/locale-default "en-us" "$(pv locale_default)" # ============================================================================= # to(): THE WALL CLOCK CHANGES AND THE INSTANT DOES NOT. # ============================================================================= # # Both halves are asserted for each of the three zones: the rendered local time # with its offset, and -- once, across all three at the end -- that the epoch # second is unchanged and the two `Arrow's compare EQUAL despite printing # differently. # # EVERY ZONE NAMED HERE IS A CANONICAL IANA NAME, never a `backward' # compatibility link. `US/Pacific' is a link to `America/Los_Angeles' in the # full database and is absent from a slim one -- the x86-64 control these # literals were measured on carries 454 zoneinfo files and has no `US/' # directory at all -- so a case naming a link would assert which tzdata build # shipped rather than anything about arrow. # # HALF-HOUR OFFSETS EXIST: India is +05:30, and a caller who stores an offset # in whole hours is wrong for a fifth of the world's population. Moscow in May # 2013 is +04:00, which is not its offset today -- Russia ran permanent DST # from 2011 to 2014 -- so that case is also a statement that the zone's HISTORY # is being read and not just its current rule. check_eq $P/to-source-offset "0" "$(pv to_source_offset)" check_eq $P/to-pacific "2013-05-11 07:23:58 -07:00" "$(pv to_pacific)" check_eq $P/to-pacific-offset "-25200" "$(pv to_pacific_offset)" check_eq $P/to-kolkata "2013-05-11 19:53:58 +05:30" "$(pv to_kolkata)" check_eq $P/to-kolkata-offset "19800" "$(pv to_kolkata_offset)" check_eq $P/to-moscow "2013-05-11 18:23:58 +04:00" "$(pv to_moscow)" check_eq $P/to-moscow-offset "14400" "$(pv to_moscow_offset)" check_eq $P/to-instant-preserved "yes" "$(pv to_instant_preserved)" check_eq $P/to-equality "yes" "$(pv to_equality)" check_eq $P/to-utc-roundtrip "2013-05-11T14:23:58.970460+00:00" "$(pv to_utc_roundtrip)" check_eq $P/to-utc-alias-offset "0" "$(pv to_utc_alias_offset)" check_eq $P/to-unknown-zone-raises "ParserError" "$(pv to_unknown_zone_raises)" # A ZONE'S NAME AND OFFSET ON EACH SIDE OF ITS OWN DST BOUNDARY, which is what # makes `tzname()' worth reporting at all. `tzdata_reachable' below catches a # database that is MISSING -- `gettz' answers None and every case in this group # reds for a reason that has nothing to do with arrow. It does NOT catch the # case this image is in: a slim TZif whose empty v1 block `dateutil' parses # into a valid, transition-free, nameless zone, which is non-None and so scores # `tzdata_reachable' green while every offset here reds at +00:00. Both belong # to tzdata's build variant rather than to arrow; see the header note above. check_eq $P/tzname-pacific-summer "PDT" "$(pv tzname_pacific_summer)" check_eq $P/tzname-pacific-winter "PST" "$(pv tzname_pacific_winter)" check_eq $P/offset-pacific-summer "-25200" "$(pv offset_pacific_summer)" check_eq $P/offset-pacific-winter "-28800" "$(pv offset_pacific_winter)" check_eq $P/tzdata-reachable "yes" "$(pv tzdata_reachable)" # ------------------------------------------------ span, floor, ceil, range --- # A SPAN IS HALF-OPEN BY A MICROSECOND, not by a second: the end is # 23:59:59.999999, which is what a caller comparing with `<=' has to know and # what `ceil_microsecond' states on its own. The week starts on MONDAY. check_eq $P/span-hour "2013-05-11T21:00:00+07:00,2013-05-11T21:59:59.999999+07:00" "$(pv span_hour)" check_eq $P/span-day "2013-05-11T00:00:00+07:00,2013-05-11T23:59:59.999999+07:00" "$(pv span_day)" check_eq $P/span-week-start "2013-05-06 Mon" "$(pv span_week_start)" check_eq $P/floor-day "2013-05-11T00:00:00+07:00" "$(pv floor_day)" check_eq $P/ceil-day "2013-05-11T23:59:59.999999+07:00" "$(pv ceil_day)" check_eq $P/ceil-microsecond "999999" "$(pv ceil_microsecond)" # `Arrow.range' RETURNS A GENERATOR. `[0]' on it is a `TypeError' -- the first # thing a caller writes and the first thing that breaks -- so the type and the # refusal are asserted before the values are. check_eq $P/range-hours-count "6" "$(pv range_hours_count)" check_eq $P/range-type "generator" "$(pv range_type)" check_eq $P/range-subscript-raises "TypeError" "$(pv range_subscript_raises)" check_eq $P/range-first-last "2013-05-05T12:30:00+00:00,2013-05-05T17:30:00+00:00" "$(pv range_first_last)" check_eq $P/span-range-count "6" "$(pv span_range_count)" check_eq $P/interval-count "4" "$(pv interval_count)" # ------------------------------------------------------------- properties --- # `.naive' DROPS THE TZINFO WITHOUT CONVERTING: it is the local wall clock as a # naive `datetime', not the UTC one. A caller who reads it as UTC is wrong by # the offset, which is the same trap `bare_date' above sets from the other # side. check_eq $P/prop-naive "2013-05-11T21:23:58.970460" "$(pv prop_naive)" check_eq $P/prop-naive-tzinfo "None" "$(pv prop_naive_tzinfo)" check_eq $P/prop-datetime-tz-offset "25200" "$(pv prop_datetime_tz_offset)" check_eq $P/prop-timestamp "1368282238.97046" "$(pv prop_timestamp)" check_eq $P/prop-int-timestamp "1368282238" "$(pv prop_int_timestamp)" check_eq $P/prop-float-timestamp "1368282238.97046" "$(pv prop_float_timestamp)" check_eq $P/prop-fields "2013,5,11,21,23,58,970460" "$(pv prop_fields)" check_eq $P/prop-isocalendar "2013,19,6" "$(pv prop_isocalendar)" check_eq $P/prop-weekday "5" "$(pv prop_weekday)" check_eq $P/prop-timetuple-yday "131" "$(pv prop_timetuple_yday)" check_eq $P/prop-for-json "2013-05-11T21:23:58.970460+07:00" "$(pv prop_for_json)" check_eq $P/prop-utcoffset-seconds "25200" "$(pv prop_utcoffset_seconds)" check_eq $P/prop-dst-seconds "0" "$(pv prop_dst_seconds)" # ARITHMETIC WITH A `timedelta' IS ABSOLUTE, which is the contrast the whole # `shift' group above is written against: `+ timedelta(days=1)' is +86400 # seconds whatever the calendar does. Subtracting two `Arrow's gives a # `timedelta', and the ordering operators work. check_eq $P/arith-add-timedelta "2013-05-06T12:30:45+00:00" "$(pv arith_add_timedelta)" check_eq $P/arith-sub-arrow-seconds "7200" "$(pv arith_sub_arrow_seconds)" check_eq $P/arith-compare "yes" "$(pv arith_compare)" # ------------------------------------------------------------- the factory --- # `ArrowFactory' is the documented way to get a SUBCLASS back out of `get', # and `arrow.api.factory' is how a caller reaches it. check_eq $P/factory-type "_MyArrow" "$(pv factory_type)" check_eq $P/factory-is-arrow "yes" "$(pv factory_is_arrow)" # ------------------------------------------------------------- the closure --- # NOTHING OUTSIDE THE STANDARD LIBRARY BUT `dateutil' IS REACHED BY THE IMPORT. # 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 third-party # dependency would fail at `import' here rather than resolve it. # # Scored against `sys.stdlib_module_names' rather than against a literal list: # what the import delta CONTAINS depends on what the interpreter had already # loaded, measured at very different sizes under a `site' and under `python3 # -S' for the same package, so a literal here would red on target for a reason # that has nothing to do with arrow (BRINTOS-2233). check_eq $P/pulled-nonstdlib "dateutil,six" "$(pv pulled_nonstdlib)" # ...AND THE FOUR NAMES THE bbappend's RDEPENDS LINE IS ABOUT, POSITIVELY. # `dateutil' is the third-party one; `calendar' and `datetime' are # python3-datetime's split and `decimal' is python3-numbers', which is why # those two packages are ADDED there. `pkg-depends-*' above is the packaging # side of the same statement. check_eq $P/needs-dateutil "yes" "$(pv needs_dateutil)" check_eq $P/needs-datetime "yes" "$(pv needs_datetime)" check_eq $P/needs-calendar "yes" "$(pv needs_calendar)" check_eq $P/needs-decimal "yes" "$(pv needs_decimal)" # ...AND THE THREE THE bbappend REMOVES, NEGATIVELY. The claim is about what # `import arrow' reaches, so each is scored against the import snapshot and not # against live `sys.modules'. `json' is imported nowhere in the package; # `dateutil.zoneinfo' is dateutil's BUNDLED tz fallback, which `dateutil.tz' # imports LAZILY on the first lookup the system database cannot answer -- a # lookup `to-unknown-zone-raises' above makes deliberately, so the live reading # reports the probe's own call and not the port's RDEPENDS; `typing_extensions' # is unreachable on 3.12 because all five of its imports sit under # `if sys.version_info < (3, 8)', which the last case pins by measuring the # interpreter the suite actually ran on. check_eq $P/no-json "yes" "$(pv no_json)" check_eq $P/no-dateutil-zoneinfo "yes" "$(pv no_dateutil_zoneinfo)" check_eq $P/no-typing-extensions "yes" "$(pv no_typing_extensions)" check_eq $P/python-major-minor "3.12" "$(pv python_major_minor)" rm -rf "$W"