. "$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 eighty measurements. # # The work inside the probe is integer arithmetic over # # dates, 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, # # 99-python3-wcwidth and 99-python3-aniso8601 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-jdatetime 5.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-jdatetime_5.0.0.bb, `inherit pypi # setuptools3', PyPI sdist, with a meta-brintos bbappend) on the wasm32-hwjs # glibc image -- together with python3-jalali-core 1.0.0, which is a recipe # THIS LAYER AUTHORS because no layer the build carries provides it. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # Jalali (Persian) `date' and `datetime' types with the same interface as the # standard library's. `jdatetime.date(1403, 1, 1)' is Nowruz 1403 and converts # to 20 March 2024; `fromgregorian' goes the other way. The Jalali year begins # at the vernal equinox, its first six months have 31 days, the next five have # 30, and Esfand has 29 -- or 30 in a leap year. # # ============================================================================ # EVERY VALUE BELOW IS A CONTROL MEASUREMENT, NOT A GUESS, AND NOT YET A # TARGET RESULT. # ============================================================================ # # The expected values were measured on an x86-64 python3.12 control against the # trees unpacked from the two PyPI sdists, by running THIS suite's own probe. # They are facts about jdatetime 5.0.0 and jalali_core 1.0.0, which are pure # Python integer arithmetic with no C extension and no platform branch, so the # arm cannot change them -- but this port's FIRST on-target run is the batch # verify, and until that runs no line here is a target measurement. # # ============================================================================ # THE PACKAGE DOES NOT IMPORT WITHOUT jalali_core, AND meta-python DOES NOT # SAY SO. # ============================================================================ # # This is the substance of the port and the reason a second recipe exists. # meta-python's recipe declares `RDEPENDS:${PN} += "python3-modules"' and # nothing else. jdatetime/__init__.py line 17 does # # from jalali_core import GregorianToJalali, JalaliToGregorian, j_days_in_month # # at MODULE scope. `jalali_core' is a SEPARATE PyPI distribution -- jdatetime's # sdist declares `install_requires=["jalali-core>=1.0"]' -- so python3-modules # cannot supply it however complete it is, and without it `import jdatetime' is # a ModuleNotFoundError on ANY image. Neither meta-openembedded nor poky carries # a recipe for it at this build's pins, so meta-brintos authors one: # recipes-devtools/python/python3-jalali-core_1.0.0.bb. # # Three cases carry that statement, at three different levels, so a regression # names its own layer rather than presenting as one broken import: # `pkg-depends-jalali-core' (the .deb declares it), `jalali-core-*' (the files # are on the rootfs), and `pulled-nonstdlib' (the import actually reached it). # # ============================================================================ # THERE IS NO `jdatetime.__version__', AND THE SUITE SAYS SO POSITIVELY. # ============================================================================ # # The module defines no `__version__' attribute -- `dir(jdatetime)' has no such # name in 5.0.0. The version is therefore asserted from the INSTALLED # DISTRIBUTION metadata (`dist-info-version', `probe-dist-version'), and # `version-attr' asserts the ABSENCE explicitly rather than leaving it untested. # A suite that asserted a module attribute here would red on a correct package, # and a reader who assumed one exists should meet the answer rather than # silence. # # ============================================================================ # THE DEFAULT LOCALE IS NOT PERSIAN. # ============================================================================ # # `strftime("%B")' on a default-locale date answers `Farvardin' -- a LATIN # transliteration -- not a name in Arabic script. Persian names need # `locale=jdatetime.FA_LOCALE' passed to the constructor. Both arms are # asserted, and the contrast is the point: a caller who expects Persian output # without asking for it gets ASCII, silently. # # THE PERSIAN NAMES ARE SCORED AS CODEPOINTS. The guest runs under the C locale, # so whether CPython coerces stdout to UTF-8 is a property of the platform # rather than of jdatetime. Printing Arabic-script names would make every case # below a test of the guest's stdout encoding; the probe emits `u0641.u0631...' # instead, which is ASCII whatever the platform does. `fa-iso-ascii' closes the # group by asserting the NUMERALS stay Western under fa_IR -- the locale changes # the names, not the digits, which is the half a caller assumes wrongly in both # directions. # # ============================================================================ # A jdatetime.date COMPARES EQUAL TO ITS GREGORIAN COUNTERPART. # ============================================================================ # # `jdatetime.date(1403,1,1) == datetime.date(2024,3,20)' is True: the comparison # converts rather than refusing on type. It means the two can collide in a set # or a dict key, and a caller who relies on type to keep calendars apart is # wrong. `eq-gregorian-same-day' asserts it and `eq-gregorian-other-day' asserts # the converse, so the pair is scoring a CONVERSION and not a blanket True. # # ============================================================================ # THE CLOCK IS AN INPUT, NOT AN AMBIENT FACT. # ============================================================================ # # Every conversion case is a committed pair of dates, so no result here depends # on when the suite runs, on the guest's timezone, or on any time source off the # machine. The one case that must read the clock -- `today()' -- is scored as a # RELATION against the Gregorian date read in the SAME interpreter start # (`today-agrees', `today-roundtrips'), never against a literal, so it is # correct on every day rather than on the day it was written. # # ============================================================================ # THE UPSTREAM TESTS ARE NOT ON THE IMAGE, AND THAT IS THE SDIST'S DOING. # ============================================================================ # # jdatetime's sdist carries tests/test_jdate.py and tests/test_jdatetime.py, but # setup.py lists `packages=['jdatetime']' explicitly rather than calling # find_packages(), and `tests' is a top-level directory rather than a subpackage. # So nothing installs them -- unlike python3-aniso8601, whose bare # find_packages() ships its tests in the runtime package. `no-tests-dir' asserts # their absence, so a reader meeting it need not wonder whether we removed # something. P=python3-jdatetime bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/jdatetime DI=$SITE/jdatetime-5.0.0.dist-info JCDIR=$SITE/jalali_core JCDI=$SITE/jalali_core-1.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-jdatetime.list JCLIST=/var/lib/dpkg/info/python3-jalali-core.list PROBE=$BST_DATA/python3-jdatetime-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-jdatetime.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------- the files --- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/jdatetime.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # jdatetime is ONE module in a package directory -- there is no submodule to # find, which is why the closure below is as small as it is. check_eq $P/one-source-file "1" \ "$(ls "$PKGDIR"/*.py 2>/dev/null | wc -l | tr -d ' ')" # The upstream tests are NOT installed -- see the header. check $P/no-tests-dir test ! -d "$PKGDIR/tests" # There is no console script; jdatetime is a library only. check $P/no-cli test ! -e /usr/bin/jdatetime # ------------------------------------------- jalali_core is on the rootfs --- # THE DEPENDENCY THIS PORT EXISTS TO SUPPLY. Asserted as files here, as a # declared Depends below, and as a reached import in the probe. check $P/jalali-core-dir test -d "$JCDIR" check $P/jalali-core-init test -s "$JCDIR/__init__.py" check $P/jalali-core-pycache test -s "$JCDIR/__pycache__/__init__.cpython-312.pyc" check $P/jalali-core-dist-info test -d "$JCDI" # ------------------------------------------------------- the package list --- # COPIED TO TMPFS BEFORE IT IS READ. A `while ... done < $PKGLIST' would read the # virtiofs root inside the loop and can end the suite; check-suite-redirects.py # refuses it, and bst_local/$TMPDIR is the sanctioned shape. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NOT ONE SHARED OBJECT. jdatetime is pure Python -- no C extension, no # accelerator arm -- which is what makes it portable to an image whose python3 # is one static binary with an empty lib-dynload and no dynamic loader # (BRINTOS-1476). check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" _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" cat "$JCLIST" > "$W/jcf" 2>/dev/null || : > "$W/jcf" check $P/jalali-core-list-present test -s "$W/jcf" check_eq $P/jalali-core-no-so "" \ "$(grep -E '\.so($|\.)' "$W/jcf" 2>/dev/null)" # ---------------------------------------------------------- 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: 5.0.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: jdatetime" cat "$DI/METADATA" check_out $P/dist-info-top-level "jdatetime" cat "$DI/top_level.txt" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # THE SDIST DECLARES THE DEPENDENCY meta-python's RECIPE OMITS, and it is in the # installed metadata where anyone can read it. This case is the documentary # evidence for the whole port: `Requires-Dist: jalali-core>=1.0'. check_out $P/dist-info-requires-jalali "jalali-core" cat "$DI/METADATA" check $P/jalali-core-metadata test -f "$JCDI/METADATA" check_out $P/jalali-core-version "Version: 1.0.0" cat "$JCDI/METADATA" # `Name:' IS MATCHED ON ITS PREFIX ON PURPOSE. The sdist's PKG-INFO says # `Name: jalali_core' with an underscore, but the METADATA that lands in the # dist-info is REGENERATED at build time and setuptools normalises the separator # in some versions and not others. `jalali' distinguishes this package from # every other on the image without pinning a spelling that is the packaging # tool's choice rather than a fact about the port. check_out $P/jalali-core-name "Name: jalali" cat "$JCDI/METADATA" # --------------------------------------------------------- the dpkg status --- sed -n '/^Package: python3-jdatetime$/,/^$/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 THREE THE BBAPPEND MAKES TRUE. python3-jalali-core is the one that CHANGES # THE ROOTFS and without which the package cannot import; python3-datetime is # the precision fix (this image installs it anyway, so it changes nothing here # and makes the package correct rather than the image different). check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-jalali-core "python3-jalali-core" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" sed -n '/^Package: python3-jalali-core$/,/^$/p' /var/lib/dpkg/status > "$W/jst" 2>/dev/null check $P/jalali-core-known test -s "$W/jst" check_out $P/jalali-core-installed "Status: install ok installed" cat "$W/jst" grep '^Depends:' "$W/jst" > "$W/jdep" 2>/dev/null || : > "$W/jdep" check_out $P/jalali-core-depends-core "python3-core" cat "$W/jdep" # NO ptest SPLIT. Neither recipe inherits ptest, so neither should have one. check_eq $P/no-ptest-package "" \ "$(sed -n '/^Package: python3-jdatetime-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # ------------------------------------------------------------- the import --- # THE CASE THE WHOLE PORT TURNS ON. Without python3-jalali-core this is a # ModuleNotFoundError at jdatetime/__init__.py line 17. check $P/import python3 -c 'import jdatetime' # ...and it says NOTHING while doing it -- no DeprecationWarning, no locale # complaint. STDERR is the assertion. check_eq $P/import-quiet "" \ "$(python3 -c 'import jdatetime' 2>&1)" # jalali_core imports standalone too, so a failure above is attributable to one # package rather than to the pair. check $P/jalali-core-import python3 -c 'import jalali_core' # ---------------------------------------------------------------- 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 below it -- see the probe's own docstring. 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 "180" "$(pv emitted_count)" # --------------------------------------------------------------- identity --- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # THE ABSENCE, ASSERTED POSITIVELY -- see the header. jdatetime 5.0.0 defines no # `__version__', so the version comes from the installed distribution metadata. check_eq $P/version-attr "" "$(pv version_attr)" check_eq $P/probe-dist-version "5.0.0" "$(pv dist_version)" check_eq $P/probe-dist-version-jalali "1.0.0" "$(pv dist_version_jalali)" # ------------------------------------------------------------ API surface --- check_eq $P/api-date "yes" "$(pv has_date)" check_eq $P/api-datetime "yes" "$(pv has_datetime)" check_eq $P/api-time "yes" "$(pv has_time)" check_eq $P/api-timedelta "yes" "$(pv has_timedelta)" check_eq $P/api-tzinfo "yes" "$(pv has_tzinfo)" check_eq $P/api-gregorian-to-jalali "yes" "$(pv has_GregorianToJalali)" check_eq $P/api-jalali-to-gregorian "yes" "$(pv has_JalaliToGregorian)" check_eq $P/api-j-days-in-month "yes" "$(pv has_j_days_in_month)" check_eq $P/api-set-locale "yes" "$(pv has_set_locale)" check_eq $P/api-get-locale "yes" "$(pv has_get_locale)" check_eq $P/api-fa-locale "yes" "$(pv has_FA_LOCALE)" check_eq $P/api-minyear "yes" "$(pv has_MINYEAR)" check_eq $P/api-maxyear "yes" "$(pv has_MAXYEAR)" check_eq $P/api-strftime-mapping "yes" "$(pv has_STRFTIME_MAPPING)" check_eq $P/minyear "1" "$(pv minyear)" check_eq $P/maxyear "9377" "$(pv maxyear)" check_eq $P/fa-locale-value "fa_IR" "$(pv fa_locale)" # The default locale is UNSET -- which is what makes strftime answer Latin. check_eq $P/default-locale-none "None" "$(pv default_locale)" # `timedelta' and `tzinfo' ARE the standard library's, re-exported rather than # reimplemented, so a caller may hand a datetime.timedelta straight to jdatetime # arithmetic. Asserted by IDENTITY, not by behaviour. check_eq $P/timedelta-is-stdlib "yes" "$(pv timedelta_is_stdlib)" check_eq $P/tzinfo-is-stdlib "yes" "$(pv tzinfo_is_stdlib)" # THE MONTH-LENGTH TABLE, read off the module rather than implied by the # conversion cases, so a release that changed one entry is NAMED here instead of # only causing a spray of reds. Six 31s, five 30s, one 29 -- and the sum is 365, # which is the common year. check_eq $P/days-in-month "31,31,31,31,31,31,30,30,30,30,30,29" "$(pv days_in_month)" check_eq $P/days-in-month-sum "365" "$(pv days_in_month_sum)" # ========================================================================= # THE CONVERSION CORPUS -- both directions and the round trip, over dates # chosen for the boundary each one sits on. The round trip is the assertion # the other two only imply: a conversion can agree with a committed literal # in both directions and still not be its own inverse. # ========================================================================= # Nowruz 1402 -- the common-year epoch this corpus starts from check_eq $P/j2g-nowruz-1402 2023-03-21 "$(pv j2g_nowruz-1402)" check_eq $P/g2j-nowruz-1402 1402-01-01 "$(pv g2j_nowruz-1402)" check_eq $P/rt-nowruz-1402 "yes" "$(pv rt_nowruz-1402)" # Farvardin has 31 days check_eq $P/j2g-farvardin-31-common 2023-04-20 "$(pv j2g_farvardin-31-common)" check_eq $P/g2j-farvardin-31-common 1402-01-31 "$(pv g2j_farvardin-31-common)" check_eq $P/rt-farvardin-31-common "yes" "$(pv rt_farvardin-31-common)" # Shahrivar is the LAST 31-day month check_eq $P/j2g-shahrivar-31-last-31-day-month 2023-09-22 "$(pv j2g_shahrivar-31-last-31-day-month)" check_eq $P/g2j-shahrivar-31-last-31-day-month 1402-06-31 "$(pv g2j_shahrivar-31-last-31-day-month)" check_eq $P/rt-shahrivar-31-last-31-day-month "yes" "$(pv rt_shahrivar-31-last-31-day-month)" # Mehr is the FIRST 30-day month -- the boundary check_eq $P/j2g-mehr-1-first-30-day-month 2023-09-23 "$(pv j2g_mehr-1-first-30-day-month)" check_eq $P/g2j-mehr-1-first-30-day-month 1402-07-01 "$(pv g2j_mehr-1-first-30-day-month)" check_eq $P/rt-mehr-1-first-30-day-month "yes" "$(pv rt_mehr-1-first-30-day-month)" # Mehr has 30, not 31 check_eq $P/j2g-mehr-30 2023-10-22 "$(pv j2g_mehr-30)" check_eq $P/g2j-mehr-30 1402-07-30 "$(pv g2j_mehr-30)" check_eq $P/rt-mehr-30 "yes" "$(pv rt_mehr-30)" # Bahman, the last 30-day month check_eq $P/j2g-bahman-30 2024-02-19 "$(pv j2g_bahman-30)" check_eq $P/g2j-bahman-30 1402-11-30 "$(pv g2j_bahman-30)" check_eq $P/rt-bahman-30 "yes" "$(pv rt_bahman-30)" # Esfand begins check_eq $P/j2g-esfand-1-common 2024-02-20 "$(pv j2g_esfand-1-common)" check_eq $P/g2j-esfand-1-common 1402-12-01 "$(pv g2j_esfand-1-common)" check_eq $P/rt-esfand-1-common "yes" "$(pv rt_esfand-1-common)" # the day before a GREGORIAN leap day check_eq $P/j2g-greg-leap-eve-inside-1402 2024-02-28 "$(pv j2g_greg-leap-eve-inside-1402)" check_eq $P/g2j-greg-leap-eve-inside-1402 1402-12-09 "$(pv g2j_greg-leap-eve-inside-1402)" check_eq $P/rt-greg-leap-eve-inside-1402 "yes" "$(pv rt_greg-leap-eve-inside-1402)" # 29 February 2024 falls inside Jalali 1402 check_eq $P/j2g-greg-leap-day-inside-1402 2024-02-29 "$(pv j2g_greg-leap-day-inside-1402)" check_eq $P/g2j-greg-leap-day-inside-1402 1402-12-10 "$(pv g2j_greg-leap-day-inside-1402)" check_eq $P/rt-greg-leap-day-inside-1402 "yes" "$(pv rt_greg-leap-day-inside-1402)" # and the day after it check_eq $P/j2g-greg-leap-day-plus-one 2024-03-01 "$(pv j2g_greg-leap-day-plus-one)" check_eq $P/g2j-greg-leap-day-plus-one 1402-12-11 "$(pv g2j_greg-leap-day-plus-one)" check_eq $P/rt-greg-leap-day-plus-one "yes" "$(pv rt_greg-leap-day-plus-one)" # the last day of a COMMON Jalali year check_eq $P/j2g-esfand-29-last-day-of-common-year 2024-03-19 "$(pv j2g_esfand-29-last-day-of-common-year)" check_eq $P/g2j-esfand-29-last-day-of-common-year 1402-12-29 "$(pv g2j_esfand-29-last-day-of-common-year)" check_eq $P/rt-esfand-29-last-day-of-common-year "yes" "$(pv rt_esfand-29-last-day-of-common-year)" # Nowruz 1403 -- a LEAP year begins check_eq $P/j2g-nowruz-1403-leap 2024-03-20 "$(pv j2g_nowruz-1403-leap)" check_eq $P/g2j-nowruz-1403-leap 1403-01-01 "$(pv g2j_nowruz-1403-leap)" check_eq $P/rt-nowruz-1403-leap "yes" "$(pv rt_nowruz-1403-leap)" # the 31-day boundary again, in a leap year check_eq $P/j2g-farvardin-31-leap 2024-04-19 "$(pv j2g_farvardin-31-leap)" check_eq $P/g2j-farvardin-31-leap 1403-01-31 "$(pv g2j_farvardin-31-leap)" check_eq $P/rt-farvardin-31-leap "yes" "$(pv rt_farvardin-31-leap)" # the last 31-day month, in a leap year check_eq $P/j2g-shahrivar-31-leap 2024-09-21 "$(pv j2g_shahrivar-31-leap)" check_eq $P/g2j-shahrivar-31-leap 1403-06-31 "$(pv g2j_shahrivar-31-leap)" check_eq $P/rt-shahrivar-31-leap "yes" "$(pv rt_shahrivar-31-leap)" # the 30-day boundary, in a leap year check_eq $P/j2g-mehr-1-leap 2024-09-22 "$(pv j2g_mehr-1-leap)" check_eq $P/g2j-mehr-1-leap 1403-07-01 "$(pv g2j_mehr-1-leap)" check_eq $P/rt-mehr-1-leap "yes" "$(pv rt_mehr-1-leap)" # the last 30-day month, in a leap year check_eq $P/j2g-bahman-30-leap 2025-02-18 "$(pv j2g_bahman-30-leap)" check_eq $P/g2j-bahman-30-leap 1403-11-30 "$(pv g2j_bahman-30-leap)" check_eq $P/rt-bahman-30-leap "yes" "$(pv rt_bahman-30-leap)" # Esfand 29 -- NOT the last day, in a leap year check_eq $P/j2g-esfand-29-leap 2025-03-19 "$(pv j2g_esfand-29-leap)" check_eq $P/g2j-esfand-29-leap 1403-12-29 "$(pv g2j_esfand-29-leap)" check_eq $P/rt-esfand-29-leap "yes" "$(pv rt_esfand-29-leap)" # the leap day itself check_eq $P/j2g-esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR 2025-03-20 "$(pv j2g_esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR)" check_eq $P/g2j-esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR 1403-12-30 "$(pv g2j_esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR)" check_eq $P/rt-esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR "yes" "$(pv rt_esfand-30-EXISTS-ONLY-IN-A-LEAP-YEAR)" # and the year that follows it check_eq $P/j2g-nowruz-1404-after-leap 2025-03-21 "$(pv j2g_nowruz-1404-after-leap)" check_eq $P/g2j-nowruz-1404-after-leap 1404-01-01 "$(pv g2j_nowruz-1404-after-leap)" check_eq $P/rt-nowruz-1404-after-leap "yes" "$(pv rt_nowruz-1404-after-leap)" # a leap year four years earlier check_eq $P/j2g-nowruz-1399-leap 2020-03-20 "$(pv j2g_nowruz-1399-leap)" check_eq $P/g2j-nowruz-1399-leap 1399-01-01 "$(pv g2j_nowruz-1399-leap)" check_eq $P/rt-nowruz-1399-leap "yes" "$(pv rt_nowruz-1399-leap)" # its leap day too check_eq $P/j2g-esfand-30-1399 2021-03-20 "$(pv j2g_esfand-30-1399)" check_eq $P/g2j-esfand-30-1399 1399-12-30 "$(pv g2j_esfand-30-1399)" check_eq $P/rt-esfand-30-1399 "yes" "$(pv rt_esfand-30-1399)" # a leap year FIVE years after 1403 -- the rule is not every-fourth check_eq $P/j2g-nowruz-1408-leap 2029-03-20 "$(pv j2g_nowruz-1408-leap)" check_eq $P/g2j-nowruz-1408-leap 1408-01-02 "$(pv g2j_nowruz-1408-leap)" check_eq $P/rt-nowruz-1408-leap "yes" "$(pv rt_nowruz-1408-leap)" check_eq $P/corpus-size "22" "$(pv corpus_size)" # ------------------------------------------------------------- leap years --- # 1399, 1403 and 1408 are leap; the rest are not. The set SPANS 1403->1408 # because that gap is FIVE years, not four: the Jalali leap rule is not a plain # every-fourth-year, and a sample that skipped the gap would pass on a # every-fourth-year implementation. check_eq $P/isleap-1399 "yes" "$(pv isleap_1399)" check_eq $P/isleap-1400 "no" "$(pv isleap_1400)" check_eq $P/isleap-1401 "no" "$(pv isleap_1401)" check_eq $P/isleap-1402 "no" "$(pv isleap_1402)" check_eq $P/isleap-1403 "yes" "$(pv isleap_1403)" check_eq $P/isleap-1404 "no" "$(pv isleap_1404)" check_eq $P/isleap-1405 "no" "$(pv isleap_1405)" check_eq $P/isleap-1406 "no" "$(pv isleap_1406)" check_eq $P/isleap-1407 "no" "$(pv isleap_1407)" check_eq $P/isleap-1408 "yes" "$(pv isleap_1408)" # The same statement in days rather than as a flag, Nowruz to Nowruz. check_eq $P/yearlen-1402 "365" "$(pv yearlen_1402)" check_eq $P/yearlen-1403 "366" "$(pv yearlen_1403)" # ESFAND 30 IS THE LEAP DAY. It exists in 1403 and RAISES in 1402 -- it does not # clamp to the 29th and does not roll into Farvardin. check_eq $P/esfand30-leap "ok:2025-03-20" "$(pv esfand30_leap)" check_eq $P/esfand30-common "ValueError" "$(pv esfand30_common)" # --------------------------------------------------------------- strftime --- check_eq $P/strftime-iso "1403-01-01" "$(pv strftime_iso)" check_eq $P/strftime-m "01" "$(pv strftime_m)" check_eq $P/strftime-y "03" "$(pv strftime_y)" check_eq $P/strftime-j "001" "$(pv strftime_j)" # THE DEFAULT LOCALE ANSWERS LATIN, NOT PERSIAN -- see the header. These four # are the contrast the fa_IR cases below are read against. check_eq $P/strftime-A "Wednesday" "$(pv strftime_A)" check_eq $P/strftime-B "Farvardin" "$(pv strftime_B)" check_eq $P/strftime-a "Wed" "$(pv strftime_a)" check_eq $P/strftime-b "Far" "$(pv strftime_b)" check_eq $P/strftime-default-ascii "yes" "$(pv strftime_default_is_ascii)" # THE DAY-OF-YEAR BOUNDARY. %j reaches 366 only in a leap year, and the last day # of a common year is 365 -- the pair a caller sizing a buffer gets wrong. check_eq $P/strftime-j-esfand30 "366" "$(pv strftime_j_esfand30)" check_eq $P/strftime-j-esfand29-common "365" "$(pv strftime_j_esfand29_common)" # The WHOLE name table, scored in one case each rather than one entry of it. check_eq $P/month-names \ "Farvardin,Ordibehesht,Khordad,Tir,Mordad,Shahrivar,Mehr,Aban,Azar,Dey,Bahman,Esfand" \ "$(pv month_names)" # THE JALALI WEEK BEGINS ON SATURDAY, which is the ordering a caller carrying a # Gregorian habit gets wrong; seven consecutive days from 1403-01-04 name it. check_eq $P/weekday-names \ "Saturday,Sunday,Monday,Tuesday,Wednesday,Thursday,Friday" \ "$(pv weekday_names)" # The documented format codes, read off the module's own mapping table, so a # release that added or dropped one is named here. check_eq $P/strftime-mapping-keys \ "%-H,%-I,%-M,%-S,%-d,%-m,%A,%B,%H,%I,%M,%S,%W,%X,%Y,%Z,%a,%b,%c,%d,%f,%j,%m,%p,%w,%x,%y,%z" \ "$(pv strftime_mapping_keys)" # ------------------------------------------------- the Persian name table --- # SCORED AS CODEPOINTS, never as text -- see the header. `Chaharshanbe' and # `Farvardin' in Arabic script. check_eq $P/fa-weekday-cp "u0686.u0647.u0627.u0631.u0634.u0646.u0628.u0647" "$(pv fa_A_cp)" check_eq $P/fa-month-cp "u0641.u0631.u0648.u0631.u062f.u06cc.u0646" "$(pv fa_B_cp)" check_eq $P/fa-not-ascii "no" "$(pv fa_is_ascii)" check_eq $P/fa-weekday-len "8" "$(pv fa_A_len)" check_eq $P/fa-month-len "7" "$(pv fa_B_len)" # THE NUMERALS STAY WESTERN under fa_IR. The locale changes the NAMES, not the # digits -- the half a caller assumes wrongly in both directions. check_eq $P/fa-iso "1403-01-01" "$(pv fa_iso)" check_eq $P/fa-iso-ascii "yes" "$(pv fa_iso_is_ascii)" # All twelve Persian month names, so the fa table is scored whole rather than # sampled at Farvardin. check_eq $P/fa-month-table \ "u0641.u0631.u0648.u0631.u062f.u06cc.u0646,u0627.u0631.u062f.u06cc.u0628.u0647.u0634.u062a,u062e.u0631.u062f.u0627.u062f,u062a.u06cc.u0631,u0645.u0631.u062f.u0627.u062f,u0634.u0647.u0631.u06cc.u0648.u0631,u0645.u0647.u0631,u0622.u0628.u0627.u0646,u0622.u0630.u0631,u062f.u06cc,u0628.u0647.u0645.u0646,u0627.u0633.u0641.u0646.u062f" \ "$(pv fa_month_cps)" # --------------------------------------------------------------- strptime --- # `%Y-%m-%d' PARSES AS JALALI, not as Gregorian -- 1403-01-01 is Nowruz, and a # parser that read the string as Gregorian would answer a year in the 1400s AD. check_eq $P/strptime-iso "1403-01-01" "$(pv strptime_iso)" # A date with no time parses to MIDNIGHT rather than to the current time. check_eq $P/strptime-midnight "00:00:00" "$(pv strptime_time_is_midnight)" # TEXT ROUND TRIP, scored as a relation so it does not restate a literal. check_eq $P/strptime-roundtrip "yes" "$(pv strptime_roundtrip)" # The leap day survives a text round trip: the parser applies the same leap rule # the constructor does, rather than accepting any day 1..30 in Esfand. check_eq $P/strptime-esfand30 "ok:1403-12-30" "$(pv strptime_esfand30)" # ------------------------------------------------------------- exceptions --- # EVERY DOCUMENTED REFUSAL IS A `ValueError', and that is the whole answer: one # `except ValueError' catches the out-of-range day, the out-of-range month, the # unparseable input and the unknown format code alike. Stated as a group of # eight rather than one case, because the VALUE of the statement is that there # is no second exception type hiding in it. check_eq $P/err-day-32 "ValueError" "$(pv err_day_32)" check_eq $P/err-day-31-in-30-month "ValueError" "$(pv err_day_31_in_30_day_month)" check_eq $P/err-day-30-common-esfand "ValueError" "$(pv err_day_30_in_common_esfand)" check_eq $P/err-day-zero "ValueError" "$(pv err_day_zero)" check_eq $P/err-month-13 "ValueError" "$(pv err_month_13)" check_eq $P/err-month-zero "ValueError" "$(pv err_month_zero)" check_eq $P/err-strptime-garbage "ValueError" "$(pv err_strptime_garbage)" check_eq $P/err-strptime-bad-format "ValueError" "$(pv err_strptime_bad_format)" # ...and the day that IS legal is legal, so the group above is scoring a range # rule rather than a blanket refusal. check_eq $P/ok-day-30-leap-esfand "ok:1403-12-30" "$(pv ok_day_30_in_leap_esfand)" # ------------------------------------------------------------- arithmetic --- # ACROSS THE JALALI YEAR BOUNDARY, which is where a Gregorian intuition fails: # `+1 day' reaches Nowruz from Esfand 30 in a leap year and from Esfand 29 in a # common one, so the SAME operation lands on the new year from different dates. check_eq $P/add-day-esfand29-leap "1403-12-30" "$(pv add_day_esfand29_leap)" check_eq $P/add-day-esfand30-leap "1404-01-01" "$(pv add_day_esfand30_leap)" check_eq $P/add-day-esfand29-common "1403-01-01" "$(pv add_day_esfand29_common)" check_eq $P/sub-day-nowruz-1404 "1403-12-30" "$(pv sub_day_nowruz_1404)" check_eq $P/sub-day-nowruz-1403 "1402-12-29" "$(pv sub_day_nowruz_1403)" # A whole leap year forward from Nowruz lands on Nowruz. check_eq $P/add-366-from-nowruz-1403 "1404-01-01" "$(pv add_366_from_nowruz_1403)" # Two days, not one, because Esfand 30 sits between them. check_eq $P/diff-across-boundary "2" "$(pv diff_across_boundary)" # THE CLOSURE: the arithmetic agrees with the Gregorian arithmetic over the same # instants. A day is a day in both calendars, and the conversion commutes with # addition. check_eq $P/arith-agrees-gregorian "yes" "$(pv arith_agrees_gregorian)" # --------------------------------------------------- comparison, ordering --- check_eq $P/cmp-lt "yes" "$(pv cmp_lt)" check_eq $P/cmp-gt "yes" "$(pv cmp_gt)" check_eq $P/cmp-eq "yes" "$(pv cmp_eq)" check_eq $P/cmp-ne "yes" "$(pv cmp_ne)" check_eq $P/cmp-le "yes" "$(pv cmp_le)" # Equal dates hash equal, so a date is usable as a dict key. check_eq $P/hash-eq "yes" "$(pv hash_eq)" check_eq $P/sorted-order "1403-01-01,1403-01-02" "$(pv sorted_order)" # Ordering holds ACROSS the year boundary, where a field-by-field compare is # right and a compare on the rendered Gregorian string is not. check_eq $P/cmp-across-year "yes" "$(pv cmp_across_year)" # A jdatetime.date COMPARES EQUAL TO ITS GREGORIAN COUNTERPART -- see the # header. The pair asserts a CONVERSION, not a blanket True: the same day is # equal, the next day is not. check_eq $P/eq-gregorian-same-day "yes" "$(pv eq_gregorian_same_day)" check_eq $P/eq-gregorian-other-day "no" "$(pv eq_gregorian_other_day)" # ------------------------------------------------------------------ today --- # THE ONLY CLOCK READ IN THE SUITE, and it is scored as a RELATION against the # Gregorian date read in the same interpreter start -- so it is correct on every # day rather than on the day it was written. check_eq $P/today-agrees "yes" "$(pv today_agrees)" check_eq $P/today-roundtrips "yes" "$(pv today_roundtrips)" check_eq $P/today-plausible "yes" "$(pv today_year_is_plausible)" check_eq $P/datetime-today-agrees "yes" "$(pv datetime_today_agrees)" # ---------------------------------------------------- the datetime class --- # The date half converts and the TIME half is carried through untouched -- a # calendar conversion must not move the clock. check_eq $P/dt-iso "1403-01-01T13:45:30" "$(pv dt_iso)" check_eq $P/dt-date-part "1403-01-01" "$(pv dt_date_part)" check_eq $P/dt-time-part "13:45:30" "$(pv dt_time_part)" check_eq $P/dt-togregorian "2024-03-20T13:45:30" "$(pv dt_togregorian)" check_eq $P/dt-strftime "13:45:30" "$(pv dt_strftime_time)" check_eq $P/dt-fromgregorian "1403-01-01T13:45:30" "$(pv dt_fromgregorian)" # ------------------------------------------------------------- the closure --- # WHAT THE IMPORT ACTUALLY REACHED. `jalali_core' is the one non-stdlib name and # it is the whole reason python3-jalali-core exists: meta-python declares this # package's runtime dependency as python3-modules alone, which cannot supply it. # If python3-jalali-core ever fell out of the image, this case NAMES the missing # module instead of leaving a ModuleNotFoundError to explain itself. # # Scored against `sys.stdlib_module_names' rather than against a literal list: # what the absolute delta contains depends on what the interpreter had already # loaded before the probe started, so a literal here would red on target for a # reason that has nothing to do with jdatetime. check_eq $P/pulled-nonstdlib "jalali_core" "$(pv pulled_nonstdlib)" check_eq $P/jalali-core-imported "yes" "$(pv jalali_core_imported)" # The three stdlib top-level modules the import needs, named positively. Only # `datetime' is outside python3-core -- poky splits datetime.py, calendar.py and # _strptime.py into their own package -- which is the packaging-side half of # `pkg-depends-datetime' above. `locale' and `platform' are core entries. check_eq $P/needs-datetime "yes" "$(pv needs_datetime)" check_eq $P/needs-locale "yes" "$(pv needs_locale)" check_eq $P/needs-platform "yes" "$(pv needs_platform)" # jalali_core REACHES NOTHING AT ALL -- it has no import statement -- which is # why its recipe's RDEPENDS is python3-core and stops there. Its API is the four # names jdatetime imports plus the Gregorian month table. check_eq $P/jalali-core-answered "$JCDIR/__init__.py" "$(pv jalali_core_file)" check_eq $P/jalali-core-api \ "GregorianToJalali,JalaliToGregorian,g_days_in_month,j_days_in_month" \ "$(pv jalali_core_api)" rm -rf "$W"