"""Drive humanize over fixed tables and print one key=value per line. ONE INTERPRETER START DOES THE WHOLE OF IT. Every python3 start on this arm is a fresh load of one large static image (99-python3 records the cost), so the suite that drives this asserts on the OUTPUT rather than calling python3 once per case. Every line is `key=value`; a key is emitted at most once. The suite reads the keys it names and ignores the rest, so adding a fact here cannot break it. Failure is reported as a key too -- `*_error' -- rather than only as an exit status, because the case that reds should say which stage stopped and why. EVERY RESULT IS A FIXED-INPUT / FIXED-OUTPUT PAIR, AND NOTHING READS THE CLOCK. humanize's whole job is to turn a number, a byte count or a duration into the words a person would use, so unlike a parser there is no independent reading to compare against -- the control HAS to be a transcribed literal. What makes that safe is that every input below is fixed and every input that could reach a clock is passed a fixed reference instant instead: * `naturaltime' takes `when=', so the tense and the magnitude are computed against 2024-01-15T12:00:00 rather than against the guest's idea of now. A suite that let it default would be asserting on a moving target and would red the first time the image's clock drifted (this image's has no RTC). * `naturaldelta' and `precisedelta' take a `timedelta' outright and never touch the clock at all. * `naturalday'/`naturaldate' are DELIBERATELY ABSENT: both compare against today's date with no injection point, so there is no way to state them as a fixed pair. What they format is `strftime', which 99-python3 already covers. WHERE THE VECTORS COME FROM. The boundary values are chosen where the UNIT CHANGES, because that is the only place the arithmetic can be wrong in a way a mid-range value would hide: 999/1000 for decimal, 1023/1024 for binary, and the same pair again in GNU form where the base is 2**10 even though the flag says nothing about it. `ordinal' is driven over exactly the cases English makes irregular -- 11, 12, 13 take `th' where 1, 2, 3 take `st'/`nd'/`rd', and 21 goes back to `st' -- which is the entire content of the function. VALUES ARE EMITTED RAW, NOT AS A repr. Every value here is already a string that a person would read; a repr would add Python's quoting to it and make the suite's literals harder to check by eye than the thing being asserted. The one exception is `all_join', which is a list flattened to a comma-separated line. """ import datetime as dt import os import sys _seen = set() def emit(key, value): if key in _seen: return _seen.add(key) # One line per fact: the suite greps by key, and an embedded newline would # land as a second, unkeyed line. print("%s=%s" % (key, str(value).replace("\n", " ").replace("\r", " "))) def fail(key, exc): emit(key, "%s: %s" % (type(exc).__name__, exc)) raise SystemExit(1) def guard(key, fn, *args, **kwargs): """Emit fn(*args)'s result, or the exception AS the value. A case whose call raised should red with the exception in `got:', not with an absent key -- an absent key is indistinguishable from a probe that died earlier, and that distinction is the whole point of `probe-complete'. """ try: emit(key, fn(*args, **kwargs)) except Exception as exc: # noqa: BLE001 - reported, not raised emit(key, "%s: %s" % (type(exc).__name__, exc)) # ------------------------------------------------------------------ the import # IMPORTING humanize READS THE INSTALLED METADATA. `humanize/__init__.py' ends # with `__version__ = importlib.metadata.version(__name__)', so the dist-info is # not merely a record of what was installed -- it is on the import path, and a # package whose metadata did not land raises PackageNotFoundError HERE, before # any formatter is reached. That is why the suite asserts the dist-info # separately: it is a dependency, not a decoration. try: import humanize except Exception as exc: # noqa: BLE001 - reported, not raised fail("import_error", exc) emit("module_file", getattr(humanize, "__file__", "")) emit("version_attr", getattr(humanize, "__version__", "")) try: import importlib.metadata as _md emit("dist_version", _md.version("humanize")) except Exception as exc: # noqa: BLE001 - reported, not raised emit("dist_version", "%s: %s" % (type(exc).__name__, exc)) # THE EXPORTED SURFACE, as the module declares it. humanize DOES declare # `__all__', so unlike a module where dir() is the only reading this is the # package's own statement of what it offers -- a release that dropped a formatter # changes this line before it changes any behaviour case. emit("all_len", len(humanize.__all__)) emit("all_join", ",".join(sorted(humanize.__all__))) # --------------------------------------------------------------- naturalsize -- # THE THREE UNIT SYSTEMS, AND THE BOUNDARIES BETWEEN UNITS. # # decimal (the default) divides by 1000, binary by 1024, and GNU by 1024 while # printing a bare letter with no space. The pairs below straddle each system's # first boundary, which is the only place the arithmetic can be wrong in a way a # mid-range value would hide: 999 vs 1000 for decimal, 1023 vs 1024 for binary. # # `gnu=True' IGNORES `binary' -- that is upstream's documented precedence, and # `gnu_binary_ignored' below drives both flags at once to state it, because a # reader looking at the signature would reasonably expect the opposite. for _n in (0, 1, 999, 1000, 1023, 1024, 3000, 1000000, 3000000, 10 ** 12): guard("nsize_dec_%d" % _n, humanize.naturalsize, _n) for _n in (1023, 1024, 3000, 1048576, 10 ** 12): guard("nsize_bin_%d" % _n, humanize.naturalsize, _n, True) for _n in (300, 1023, 1024, 3000, 1048576): guard("nsize_gnu_%d" % _n, humanize.naturalsize, _n, False, True) # NEGATIVE, which is a separate code path: the sign is carried out of the # magnitude and put back on the formatted result. guard("nsize_neg", humanize.naturalsize, -4096, True) # THE PRECEDENCE, stated. binary=True AND gnu=True must give the GNU answer. guard("nsize_gnu_binary_ignored", humanize.naturalsize, 3000, True, True) # A CUSTOM FORMAT is forwarded to the `%' operator. A signature that had dropped # the parameter would pass every case above and silently ignore it here. guard("nsize_fmt", humanize.naturalsize, 3000, False, True, "%.3f") # ------------------------------------------------------------------ intcomma -- # THOUSANDS SEPARATORS over the magnitudes where a group is added. The float and # the string inputs are here because intcomma accepts both and takes a different # branch for each; a version that lost the string branch would return the input # unchanged, which reads as a pass to anything that only checks the type. for _v in (100, 1000, 10123, 10311, 1000000, 1234567890): guard("intcomma_%s" % _v, humanize.intcomma, _v) guard("intcomma_float", humanize.intcomma, 1234567.25) guard("intcomma_str", humanize.intcomma, "1234567.1234") # ndigits ROUNDS AND PADS, which is the parameter's whole purpose. guard("intcomma_ndigits", humanize.intcomma, 1234567.1234, 2) # ------------------------------------------------------------------- intword -- # THE DOCUMENTED MAGNITUDES, one vector per name upstream knows. Below a million # intword returns the number unchanged, so `intword_100' is the case that says # where the function starts applying. for _v in (100, 1000000, 1200000, 1290000, 1000000000, 2000000000, 1000000000000, 1000000000000000, 1000000000000000000): guard("intword_%d" % _v, humanize.intword, _v) # A CUSTOM FORMAT, forwarded the same way naturalsize's is. guard("intword_fmt", humanize.intword, 1230000, "%0.2f") # ------------------------------------------------------------------- ordinal -- # THE CASES ENGLISH MAKES IRREGULAR, which is the entire content of the function. # 11, 12 and 13 take `th' where 1, 2 and 3 take `st', `nd' and `rd'; 21 goes back # to `st'. An implementation that keyed on the last digit alone passes 1, 2, 3 # and 21 and fails exactly the middle three -- so the set below is chosen to # separate those two implementations rather than to sample the range. for _v in (0, 1, 2, 3, 4, 11, 12, 13, 21, 22, 23, 101, 111): guard("ordinal_%d" % _v, humanize.ordinal, _v) # --------------------------------------------------------------- naturaldelta -- # A TIMEDELTA IN, WORDS OUT, AND NO CLOCK ANYWHERE. Each vector sits either side # of one of the function's fuzziness thresholds -- a second, a minute, an hour, a # day, a month (30.5 days), a year -- because between thresholds the answer is a # division and at them it is a decision. _DELTAS = ( ("0s", dt.timedelta(seconds=0)), ("1s", dt.timedelta(seconds=1)), ("30s", dt.timedelta(seconds=30)), ("60s", dt.timedelta(seconds=60)), ("90s", dt.timedelta(seconds=90)), ("1h", dt.timedelta(hours=1)), ("2h", dt.timedelta(hours=2)), ("1d", dt.timedelta(days=1)), ("2d", dt.timedelta(days=2)), ("30d", dt.timedelta(days=30)), ("60d", dt.timedelta(days=60)), ("365d", dt.timedelta(days=365)), ("800d", dt.timedelta(days=800)), ) for _name, _d in _DELTAS: guard("ndelta_%s" % _name, humanize.naturaldelta, _d) # A BARE NUMBER OF SECONDS is accepted too, and must agree with the timedelta of # the same length. A version that lost the coercion returns the int unchanged. guard("ndelta_int", humanize.naturaldelta, 90) # months=False replaces the month band with days, which is the parameter's whole # effect and is invisible on every vector under 30.5 days. guard("ndelta_nomonths", humanize.naturaldelta, dt.timedelta(days=60), False) # ---------------------------------------------------------------- naturaltime -- # THE SAME MAGNITUDES, WITH TENSE, AGAINST A FIXED REFERENCE INSTANT. `when=' # is what makes this assertable at all: without it naturaltime reads the clock, # and on an image whose guest has no RTC the answer would depend on what the # host handed it at boot. WHEN is 2024-01-15T12:00:00, naive, and every value # below is an OFFSET FROM IT computed here rather than a literal datetime, so the # pair (input, expected) cannot drift apart. WHEN = dt.datetime(2024, 1, 15, 12, 0, 0) emit("when", WHEN.isoformat()) for _name, _d in (("30s", dt.timedelta(seconds=30)), ("90s", dt.timedelta(seconds=90)), ("2h", dt.timedelta(hours=2)), ("1d", dt.timedelta(days=1)), ("400d", dt.timedelta(days=400))): guard("ntime_past_%s" % _name, humanize.naturaltime, WHEN - _d, when=WHEN) guard("ntime_future_%s" % _name, humanize.naturaltime, WHEN + _d, when=WHEN) # THE ZERO CASE is its own word -- `now', not "0 seconds ago" -- and it is the # one branch naturaltime has that naturaldelta does not. guard("ntime_now", humanize.naturaltime, WHEN, when=WHEN) # ---------------------------------------------------------------- precisedelta -- # THE FORMATTER THAT DOES NOT ROUND AWAY THE SMALL UNITS. Where naturaldelta # picks one unit and fuzzes, precisedelta enumerates every unit that carries # something -- so the vectors below are chosen to have several non-zero units at # once, which is the only shape that distinguishes the two functions. _PD = dt.timedelta(days=2, seconds=3633, microseconds=123000) guard("pd_default", humanize.precisedelta, _PD) guard("pd_fmt", humanize.precisedelta, _PD, format="%0.4f") guard("pd_microseconds", humanize.precisedelta, _PD, minimum_unit="microseconds") guard("pd_suppress_days", humanize.precisedelta, _PD, suppress=["days"]) guard("pd_minutes", humanize.precisedelta, dt.timedelta(minutes=90), minimum_unit="minutes") guard("pd_one_second", humanize.precisedelta, dt.timedelta(seconds=1)) # ---------------------------------------------------------------------- i18n -- # THE PATH THAT IS FIRST TO BREAK ON A TRIMMED IMAGE, AND WHY IT SHOULD NOT. # # `activate("ru_RU")' is NOT setlocale(3). It is # `gettext.translation("humanize", , ["ru_RU"])' -- # a lookup for a .mo file INSIDE site-packages, with no reference to the system # locale set, LANG, or anything glibc knows about. So the question this section # answers is not "does the image carry ru_RU" (it does not, and does not need # to); it is "did the wheel ship its catalogues". # # That is a real question here rather than a rhetorical one. The .mo files are # GITIGNORED upstream and reach the wheel only because pyproject.toml names # `[tool.hatch.build] artifacts = ["*.mo"]'; a build that lost that line would # install a humanize whose every formatter worked and whose activate() raised # FileNotFoundError. `locale_dir', `mo_count' and `ru_activate' are the three # readings of that one fact, from the coarsest to the one a caller would hit. emit("locale_dir", os.path.join(os.path.dirname(humanize.__file__), "locale")) try: _ld = os.path.join(os.path.dirname(humanize.__file__), "locale") _mo = 0 for _root, _dirs, _files in os.walk(_ld): _mo += sum(1 for _f in _files if _f.endswith(".mo")) emit("mo_count", _mo) emit("ru_mo_present", "yes" if os.path.isfile( os.path.join(_ld, "ru_RU", "LC_MESSAGES", "humanize.mo")) else "no") except Exception as exc: # noqa: BLE001 - reported, not raised emit("mo_count", "%s: %s" % (type(exc).__name__, exc)) # THE CATALOGUE ACTUALLY ANSWERS. An `activate' that merely returned without # raising would pass a boolean check while translating nothing, so the assertion # is on the TRANSLATED TEXT: with ru_RU active, naturaldelta(60 s) must come back # in Cyrillic. The value is emitted as-is; the suite compares it against the # literal from the catalogue. try: humanize.activate("ru_RU") emit("ru_activate", "yes") except Exception as exc: # noqa: BLE001 - reported, not raised emit("ru_activate", "%s: %s" % (type(exc).__name__, exc)) # THE RUSSIAN ANSWERS ARE ASSERTED AS UTF-8 BYTES, IN HEX, NOT AS CYRILLIC # LITERALS. The suite is `ash' script read off a virtiofs root and its results # cross a serial console to the host, so a Cyrillic literal in the .test file # would be asserting the whole of that transport as well as the catalogue -- and # a red would not say which of the two moved. The hex is exact, is pure ASCII # end to end, and each is commented in the suite with what it decodes to. # # The raw string is emitted too, as `*_raw', but the suite does NOT score it: it # is there so a person reading a capture can see the words rather than a digest. def _ru(key, fn, *args, **kwargs): try: val = fn(*args, **kwargs) except Exception as exc: # noqa: BLE001 - reported, not raised emit(key, "%s: %s" % (type(exc).__name__, exc)) return emit(key, val.encode("utf-8").hex()) emit(key + "_raw", val) _ru("ru_ndelta_60s", humanize.naturaldelta, dt.timedelta(seconds=60)) _ru("ru_ndelta_1d", humanize.naturaldelta, dt.timedelta(days=1)) _ru("ru_ntime_2h", humanize.naturaltime, WHEN - dt.timedelta(hours=2), when=WHEN) # ...AND IT IS NOT THE ENGLISH STRING. The hex cases above are exact but opaque; # this one states the SEMANTIC point -- that a translation happened at all -- in # a form that stays true if upstream ever revises the Russian wording. try: emit("ru_translated", "yes" if humanize.naturaldelta(dt.timedelta(seconds=60)) != "a minute" else "no") except Exception as exc: # noqa: BLE001 - reported, not raised emit("ru_translated", "%s: %s" % (type(exc).__name__, exc)) # ...AND `deactivate()' RESTORES IT. This is the half that a suite driving only # activate would miss: a deactivate that did not reset the thread-local would # leave every later caller in Russian, and the failure would surface far from # here. The restored answers are compared against the SAME keys measured before # activate, so the two are stated as an equality rather than as a second literal. try: humanize.deactivate() emit("ru_deactivate", "yes") except Exception as exc: # noqa: BLE001 - reported, not raised emit("ru_deactivate", "%s: %s" % (type(exc).__name__, exc)) guard("post_ndelta_60s", humanize.naturaldelta, dt.timedelta(seconds=60)) guard("post_ndelta_1d", humanize.naturaldelta, dt.timedelta(days=1)) # A LOCALE THE PACKAGE DOES NOT CARRY must RAISE rather than fall back silently. # Without this, a broken lookup that returned NullTranslations for everything # would pass every case above -- the English answers would be "correct" because # nothing was ever translated. try: humanize.activate("zz_ZZ") emit("missing_locale", "no-raise") except Exception as exc: # noqa: BLE001 - reported, not raised emit("missing_locale", type(exc).__name__) humanize.deactivate() # The separators the module exposes for the locales that need them. These are a # plain dict lookup with no catalogue behind them, so they work on an image that # shipped no .mo at all -- which is exactly why they are asserted SEPARATELY from # the activate cases: together they say whether a red is in the lookup or in the # catalogue. try: humanize.activate("de_DE") emit("de_thousands", humanize.thousands_separator()) emit("de_decimal", humanize.decimal_separator()) humanize.deactivate() except Exception as exc: # noqa: BLE001 - reported, not raised emit("de_thousands", "%s: %s" % (type(exc).__name__, exc)) # ------------------------------------------------------------------------------ emit("py_version", "%d.%d" % sys.version_info[:2]) emit("probe_complete", "yes")