. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # other assertion reads the filesystem or dpkg with shell # # builtins, or greps the single probe run's output. # # # # WHERE THE THREE GO: the probe, one bare import case, and # # one case asserting the module resolves out of # # site-packages rather than a stray. This package ships NO # # executable, so there is no script group and no start per # # script. # # # # 1200, the code-only figure, and NOT python3-pytz's 1500: # # that budget is bought by a probe that loads 596 TZif # # binaries off the virtiofs root. Nothing here touches the # # filesystem after the import -- every instant this probe # # measures is a value it supplied itself. # # # # 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-freezegun 1.4.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-freezegun_1.4.0.bb, `inherit pypi # python_setuptools_build_meta ptest', PyPI sdist) on the wasm32-hwjs glibc # image -- the library that freezes time for tests. # # ============================================================================ # THE CLOCK IS AN INPUT HERE, AND THAT IS WHY THIS SUITE CAN ASSERT VALUES. # ============================================================================ # # Every instant below is one the probe SUPPLIED to freeze_time, so no case # depends on when the suite runs, on the guest's timezone, or on any time source # off the machine. `frozen_now=2026-09-02T12:00:00' is not a reading; it is the # argument, handed back. That is the whole purpose of the package, and it makes # this the rare port whose behaviour can be pinned exactly rather than bounded. # # The only two facts here ABOUT the real clock are deliberately not values: that # the substituted classes are put back (a class identity), and that the restored # `time.time()' is not still the frozen epoch. A case asserting the real clock # is "about right" would tie this suite to the guest's clock -- the one thing # freezegun exists to remove from an answer. # # ============================================================================ # UPSTREAM'S OWN TESTS ARE NOT ON THIS IMAGE, BY TWO INDEPENDENT ROUTES. # ============================================================================ # # The ticket that opened this port expected the recipe's `inherit ptest' to # stage upstream's suite and run it on target. It does not, and neither route # reaches this image: # # 1. brintos.conf sets DISTRO_FEATURES = "ipv4 x11 wayland". ptest.bbclass # adds ${PN}-ptest only when "ptest" is in DISTRO_FEATURES, so no -ptest # package is produced and the recipe's do_install_ptest never runs. # 2. setup.cfg declares an explicit `packages = freezegun', so `tests/' is not # a package and this .deb ships no part of it -- unlike python3-aniso8601, # whose bare find_packages() ships nine unittest files into the runtime # .deb. Asserted below against the package's own file list: the # site-packages `tests' PATH is not evidence either way, because it is a # shared namespace another distribution may occupy, and on this image # python3-reedsolo does. # # Adding "ptest" to DISTRO_FEATURES would re-sign the whole target graph, which # is not a per-package port's change to make. Both absences are ASSERTED below # rather than assumed, and the cases in this file stand in their place -- the # same disposition already recorded for python3-pytz. # # ============================================================================ # THE RECIPE UNDER-DECLARED ITS RUNTIME CLOSURE, AND THE Depends LINE IS PINNED. # ============================================================================ # # meta-python declares python3-asyncio, python3-dateutil and python3-unittest. # All three are genuinely reached -- but api.py's module-scope import block is # fifteen names long, and three more poky SPLITS own the rest: python3-datetime # (datetime AND calendar), python3-netclient (uuid, whose clock-based generators # freezegun patches), python3-numbers (numbers). The bbappend adds them; the # whole Depends line is pinned by name below, so a release that drops one reds # rather than only moving a count. # # It changes no rootfs -- python3-asyncio already rdepends on netclient and # numbers, and netclient on datetime -- which is exactly why the omission would # have stayed invisible here. Measured by refusal on an x86-64 control, not read # off the manifest; the measurement is recorded in the bbappend. # # ============================================================================ # THREE MEASURED FACTS A READER WOULD GUESS WRONG. # ============================================================================ # # 1. Under `auto_tick_seconds=N' the FIRST read returns the start instant # untouched; the advance happens between reads. So the deltas asserted are # between consecutive reads, and `auto_tick_first' is the argument itself. # # 2. `datetime.datetime.now()' answers a `FakeDatetime', not a `datetime' -- # and it is still an `isinstance' of `datetime'. Both halves are asserted, # because a port that got the type swap right and the inheritance wrong # would look correct until a caller type-checked. # # 3. A naive instant string is read as UTC. `freeze_time("2026-09-02 # 12:00:00")' makes `time.time()' exactly 1788350400.0, which is that # instant's UTC epoch -- so `utcnow()' and `now()' agree and only # `tz_offset' separates them. P=python3-freezegun SUITE_TIMEOUT_HINT=1200 bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/freezegun MOD=$PKGDIR/__init__.py DI=$SITE/freezegun-1.4.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-freezegun.list PROBE=$BST_DATA/python3-freezegun-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-freezegun.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------- the artifact check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/module-file test -f "$MOD" # A PACKAGE directory, not a flat module. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/freezegun.py" ]; then echo present; else echo absent; fi)" for _f in api _async config; do check $P/module-$_f test -f "$PKGDIR/$_f.py" done # The typing payload. setup.cfg sets `include_package_data = true' and # MANIFEST.in ships them, so a stub file missing here is a packaging loss rather # than an upstream choice -- and this is a `py.typed' package, so a consumer # running mypy against it reads exactly these three files. check $P/typing-stub-api test -f "$PKGDIR/api.pyi" check $P/typing-stub-init test -f "$PKGDIR/__init__.pyi" check $P/typing-marker test -f "$PKGDIR/py.typed" check $P/pycache-dir test -d "$PKGDIR/__pycache__" # ---------------------------------------------------------------- the package # The file list is COPIED to $TMPDIR before it is read, because the `while read' # below takes it on stdin: a top-level stdin redirect on the virtiofs root makes # dup2(2) return 513 and `ash' exits, ending the suite in silence. # # BRINTOS-WORKAROUND: brintos/linux#73 remove cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION. This image's python3 is one static binary with an empty # lib-dynload and no dynamic loader, so a `.so' in this package could not be # loaded at all -- the assertion is about portability, not tidiness. It is a # pure-Python package upstream, so a `.so' appearing here would mean the wheel # was cut on a machine whose build backend compiled something. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # Everything shipped is Python source, its bytecode, a type stub, the py.typed # marker, or dist-info metadata. A file outside that set is a compiled artifact # or a stray. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.pyi) continue ;; */py.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" sed -n '/^Package: python3-freezegun$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 1.4.0-r0" 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 RUNTIME CLOSURE, BY NAME. Three of these six were declared by # meta-python and three are added by this port's bbappend; naming each one # separately means a dropped entry names itself in the red. for _d in asyncio core datetime dateutil netclient numbers unittest; do check_out $P/pkg-depends-$_d "python3-$_d" cat "$W/dep" done # NO -ptest PACKAGE, AND NO UPSTREAM TESTS BY THE OTHER ROUTE EITHER. Both are # asserted rather than assumed; see this file's header for why neither reaches # this image and why nothing here tries to open them. check_eq $P/pkg-no-ptest-dir "absent" \ "$(if [ -d /usr/lib/python3-freezegun/ptest ]; then echo present; else echo absent; fi)" check_eq $P/pkg-no-ptest-status "absent" \ "$(if grep -q '^Package: python3-freezegun-ptest$' /var/lib/dpkg/status 2>/dev/null; \ then echo present; else echo absent; fi)" check_eq $P/pkg-no-tests-dir "absent" \ "$(if [ -d "$PKGDIR/tests" ]; then echo present; else echo absent; fi)" # Asked of THIS PACKAGE'S OWN FILE LIST, not of the directory. A top-level # `tests' in site-packages is a SHARED namespace path that any distribution may # claim -- on this image python3-reedsolo 2.0.13 owns it, with three files of # its own -- so `[ -d "$SITE/tests" ]' answers a question about the image and # reds on a package this port never touches. check_eq $P/pkg-no-tests-module "" \ "$(grep -E '/site-packages/tests(/|$)' "$W/pkgf" 2>/dev/null)" # ------------------------------------------------------------- the metadata 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.4.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: freezegun" cat "$DI/METADATA" check_out $P/dist-info-wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-wheel-tag "Tag: py3-none-any" cat "$DI/WHEEL" # BUILT BY bdist_wheel OVER setuptools, which is what `python_setuptools_build_meta' # resolves to here -- and it is the reason there is a dist-info to read at all # rather than the egg-info a plain `setuptools3' recipe leaves. A port reading # the module name out of `top_level.txt' finds it here and finds nothing in a # flit-built sibling, so its presence is asserted rather than assumed. check_out $P/dist-info-wheel-generator "bdist_wheel" cat "$DI/WHEEL" check_eq $P/dist-info-top-level "freezegun" "$(cat "$DI/top_level.txt" 2>/dev/null)" # The two files upstream's MANIFEST.in puts in the wheel's metadata directory # rather than in the package -- so the licence ships with this package and is # NOT in the runtime tree, which is what `pkg-only-python' above would otherwise # have to explain. check $P/dist-info-license test -f "$DI/LICENSE" check $P/dist-info-record test -f "$DI/RECORD" # THE ONE DECLARED INSTALL REQUIREMENT, carried into the wheel metadata -- # `python-dateutil >= 2.7' in setup.cfg. It is the PyPI name, not the poky # package name, and the two are asserted in different places for that reason: # this case reads the wheel, and pkg-depends-dateutil above reads dpkg. check_out $P/dist-info-requires-dateutil "Requires-Dist: python-dateutil" \ cat "$DI/METADATA" # ----------------------------------------------------------------- the import check $P/import python3 -c 'import freezegun; freezegun.freeze_time' # The module resolves out of site-packages rather than a stray on the path -- # the case that separates "the package shipped" from "something importable is # called freezegun". check_out $P/import-from-site "$PKGDIR" \ python3 -c 'import freezegun, os; print(os.path.dirname(freezegun.__file__))' # ------------------------------------------------------------------ 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 # 900s for one python3 start and ~40 context-manager entries. Nothing in the # probe reads the filesystem after the import. 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_complete)" check_eq $P/probe-no-import-error "" "$(pv import_error)" # ------------------------------------------------- identity and provenance check_eq $P/version "1.4.0" "$(pv version)" check_eq $P/title "freezegun" "$(pv title)" check_eq $P/license "Apache License 2.0" "$(pv license)" check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/module-is-package "yes" "$(pv module_is_package)" check_eq $P/python-version "3.12" "$(pv python_version)" # The metadata read through importlib rather than off the filesystem. check_eq $P/dist-version "1.4.0" "$(pv dist_version)" check_eq $P/dist-name "freezegun" "$(pv dist_name)" # ------------------------------------------------------------------ the surface # TWO NAMES, and that is the whole public API. A package this small is worth # pinning exactly: an export appearing here is an upstream change, not noise. check_eq $P/surface-all "configure|freeze_time" "$(pv dunder_all)" check_eq $P/surface-n-all "2" "$(pv n_dunder_all)" check_eq $P/surface-has-freeze-time "yes" "$(pv has_freeze_time)" check_eq $P/surface-has-configure "yes" "$(pv has_configure)" # THE UNPATCHED CALLABLES FREEZEGUN KEEPS. These are what make the restoration a # swap rather than a guess, and the group is asserted by name because it is also # the inventory of WHAT this package patches: three datetime/date entry points, # six from `time', and two of `uuid''s clock-based generators. That last pair is # why python3-netclient is in this package's runtime closure at all. for _n in real_datetime real_date real_time real_gmtime real_localtime \ real_strftime real_monotonic real_perf_counter real_time_ns \ real_uuid_generate_time real_uuid_create; do check_eq $P/api-has-$(echo "$_n" | tr _ -) "yes" "$(pv api_has_$_n)" done # The ignore list a caller extends with `freezegun.configure()'. Pinned as a # count plus one membership rather than the whole list: the count is the # structural signal, and `threading' is the entry that matters here, because a # thread module frozen along with everything else is how a freeze deadlocks. check_eq $P/config-ignore-count "12" "$(pv cfg_default_ignore_n)" check_eq $P/config-ignore-threading "yes" "$(pv cfg_default_ignore_has_threading)" # ============================================================================ # THE FIXED INSTANT, AGREED ON BY THREE INDEPENDENT CLOCKS. # ============================================================================ # The load-bearing group of this port, and the ticket's first assertion. # datetime.datetime.now(), datetime.date.today() and time.time() are three # separate patch sites; a port that got one and missed another would pass an # import test and answer the wrong thing under a freeze. check_eq $P/freeze-now "2026-09-02T12:00:00" "$(pv frozen_now)" check_eq $P/freeze-utcnow "2026-09-02T12:00:00" "$(pv frozen_utcnow)" check_eq $P/freeze-today "2026-09-02" "$(pv frozen_today)" check_eq $P/freeze-time "1788350400.0" "$(pv frozen_time)" # The three agreeing with EACH OTHER, not just each with the argument -- which # is the assertion that survives a change to the instant. check_eq $P/freeze-now-date-is-today "yes" "$(pv now_date_equals_today)" check_eq $P/freeze-time-is-utcnow "yes" "$(pv time_equals_utcnow)" # A NAIVE INSTANT IS READ AS UTC. The epoch above is that instant's UTC epoch, # recomputed by the probe rather than trusted from its own constant. check_eq $P/freeze-time-is-instant-epoch "yes" "$(pv frozen_time_is_instant_epoch)" check_eq $P/freeze-instant-epoch-is-utc "yes" "$(pv instant_epoch_is_utc)" # time.gmtime() is a fourth path to the same instant, through the C layer. check_eq $P/freeze-gmtime-year "2026" "$(pv gmtime_year)" check_eq $P/freeze-gmtime-mon "9" "$(pv gmtime_mon)" check_eq $P/freeze-gmtime-mday "2" "$(pv gmtime_mday)" # FROZEN MEANS IT DOES NOT ADVANCE between two reads -- including monotonic, # which a caller reaches for precisely to escape wall-clock games. check_eq $P/freeze-time-stable "yes" "$(pv time_stable)" check_eq $P/freeze-monotonic-stable "yes" "$(pv monotonic_stable)" # THE SUBSTITUTED TYPES, and their inheritance. Both halves: see the header. check_eq $P/freeze-now-type "FakeDatetime" "$(pv now_type)" check_eq $P/freeze-today-type "FakeDate" "$(pv today_type)" check_eq $P/freeze-now-isinstance-datetime "yes" "$(pv now_isinstance_datetime)" check_eq $P/freeze-today-isinstance-date "yes" "$(pv today_isinstance_date)" # ============================================================================ # tick() ADVANCES BY EXACTLY THE DOCUMENTED AMOUNT. # ============================================================================ # The ticket's third assertion, first half. Each delta is taken against the # instant's own epoch, so it is an absolute statement rather than a difference # between two things that could both be wrong -- and they are cumulative, which # is what says tick() moves the clock rather than re-setting it. check_eq $P/tick-default "1.0" "$(pv tick_default)" check_eq $P/tick-after-10 "11.0" "$(pv tick_after_10)" check_eq $P/tick-after-timedelta "11.5" "$(pv tick_after_timedelta)" # The datetime clock moved with it: tick() is one clock advancing, not two. check_eq $P/tick-now "2026-09-02T12:00:11.500000" "$(pv tick_now)" # STILL FROZEN AFTER TICKING. A tick sets a new instant; it does not start the # clock running, which is the misreading that would make every later assertion # in a caller's test flaky rather than wrong. check_eq $P/tick-still-stable "yes" "$(pv tick_still_stable)" # move_to() sets an absolute instant rather than a delta. check_eq $P/move-to-now "2026-09-03T00:00:00" "$(pv move_to_now)" check_eq $P/move-to-today "2026-09-03" "$(pv move_to_today)" # ============================================================================ # auto_tick_seconds ADVANCES BY EXACTLY THE DOCUMENTED AMOUNT, PER READ. # ============================================================================ # The ticket's third assertion, second half. The FIRST read returns the start # instant untouched -- see the header -- so the deltas are between consecutive # reads and `auto-tick-first' is the argument handed back. check_eq $P/auto-tick-first "2026-09-02T12:00:00" "$(pv auto_tick_first)" check_eq $P/auto-tick-d1 "15.0" "$(pv auto_tick_d1)" check_eq $P/auto-tick-d2 "15.0" "$(pv auto_tick_d2)" check_eq $P/auto-tick-uniform "yes" "$(pv auto_tick_uniform)" # A FRACTIONAL amount, so the advance is read off the argument rather than # rounded to a whole second somewhere in the path. check_eq $P/auto-tick-fractional "0.25" "$(pv auto_tick_fractional)" # ============================================================================ # THE REAL CLOCK IS RESTORED ON EXIT. # ============================================================================ # The ticket's second assertion, second half -- and asserted as a CLASS IDENTITY # rather than a value, deliberately. See the header. check_eq $P/restore-class-before "datetime" "$(pv real_class_before)" check_eq $P/restore-class-after "datetime" "$(pv real_class_after)" check_eq $P/restore-date-class-after "date" "$(pv real_date_class_after)" check_eq $P/restore-to-before "yes" "$(pv restored_to_before)" # The restored objects are the ones freezegun saved, not lookalikes. check_eq $P/restore-datetime-is-real "yes" "$(pv datetime_is_real_datetime)" check_eq $P/restore-time-is-real "yes" "$(pv time_is_real_time)" # And the restored clock is not still reading the frozen epoch. check_eq $P/restore-time-not-frozen "yes" "$(pv real_time_not_frozen)" # ------------------------------------------------------------- the other forms # The decorator, which is how the package is used in practice -- and the # restoration after it returns, which is the half that leaks. check_eq $P/decorator "2020-01-01|2020-01-01T00:00:00" "$(pv decorator)" check_eq $P/decorator-restored "yes" "$(pv decorator_restored)" # tz_offset shifts LOCAL time and leaves UTC alone. The asymmetry is the point: # a caller that reads utcnow() sees no offset at all. check_eq $P/tz-offset-now "2026-09-02T15:00:00" "$(pv tz_now)" check_eq $P/tz-offset-utcnow "2026-09-02T12:00:00" "$(pv tz_utcnow)" # The explicit start()/stop() API, for a caller that cannot use a context # manager, and the restoration after stop(). check_eq $P/startstop-now "1999-12-31T23:59:59" "$(pv startstop_now)" check_eq $P/startstop-class-after "datetime" "$(pv startstop_class_after)" # NESTING: the inner freeze wins, and unwinding restores the OUTER one rather # than the real clock. A port that restored to the real clock here would pass # every case above and break any test that nests. check_eq $P/nested-inner "2002-02-02T02:02:02" "$(pv nested_inner)" check_eq $P/nested-outer-restored "2001-01-01T01:01:01" "$(pv nested_outer_restored)" check_eq $P/nested-class-after "datetime" "$(pv nested_class_after)" # Three input shapes, one answer -- and a bare date freezes at midnight, which # is the one of the three a reader would have to check. check_eq $P/accepts-datetime "2026-09-02T12:00:00" "$(pv accepts_datetime)" check_eq $P/accepts-date "2026-09-02T00:00:00" "$(pv accepts_date)" rm -rf "$W"