. "$BST_LIB" # BST-TIMEOUT: 2400 # FIVE 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 five. # # # # They are a bare `import', a second one whose STDERR is the # # assertion (`import-quiet'), one that imports all seven # # submodules together, the probe -- ONE start for a hundred # # and thirty measurements -- and the threads-boundary probe, # # which is separate for the reason given at that case. # # # # The work inside the probe is calendar arithmetic over # # instants the probe itself supplies, microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 2400 is 480s per start. 99-python3-dateutil budgets 400s # # for each of three; this suite takes two more and rounds up, # # because a suite-timeout red is a BATCH-WIDE cost and the # # right fix for a slow arm 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-apscheduler 3.10.4 (meta-openembedded/meta-oe # recipes-devtools/python/python3-apscheduler_3.10.4.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # The Advanced Python Scheduler: run a callable later, once or repeatedly. Three # parts, and only the first two are portable to an image with no running threads. # # TRIGGERS `CronTrigger', `IntervalTrigger' and `DateTrigger' each answer # `get_next_fire_time(previous, now)' as a PURE FUNCTION of the two # instants handed in. The whole trigger surface is therefore # assertable with no scheduler running and nothing sleeping, which # is what most of this suite does. # # JOB STORES `MemoryJobStore' holds Job objects in a dict. The persistent # stores are out of scope -- see the scoping section below. # # SCHEDULERS A scheduler is a loop that asks the stores what is due and hands # it to an executor. `BackgroundScheduler' runs that loop in a # THREAD, which is where this arm stops it. # # ============================================================================ # THIS PORT IS SCOPED AT THE IN-MEMORY SCHEDULER, DELIBERATELY. # ============================================================================ # # APScheduler's optional job stores reach for SQLAlchemy, MongoDB, Redis, # RethinkDB and ZooKeeper, and its optional executors for Twisted, Tornado, Qt # and gevent. NONE of those distributions is on this image and none is needed: # what this port ships is the default memory store and the synchronous, thread # and asyncio executors. # # That scoping is asserted rather than left to be inferred from what happens to # be installed, and it is asserted as TWO separate facts because they move # independently. The alias tables (`aliases-*') come from apscheduler's OWN # entry_points metadata and therefore name every optional backend whether or not # it is installed; the load attempts (`optional-*-refused') are what fail on the # missing third-party import. An image that later grew SQLAlchemy would flip the # second without touching the first, and that is the signal worth having. # # ============================================================================ # THE CLOCK IS AN INPUT, NEVER AN AMBIENT FACT. # ============================================================================ # # Every instant this suite scores is one the probe supplied. No case reads # `datetime.now()' for a value it compares, so nothing here moves with WHEN the # suite runs, with the target's local zone, or with any time source off the # machine. # # That constraint is not free, and the place it costs something is the scheduler # section. Left to compute their own first fire, an interval and a cron trigger # answer from `now' FORWARD, so whether either had come due by the time the # scheduler processed them would depend on how many seconds the interpreter # spent on the calls in between -- on a slow arm, a case that flaps. The probe # therefore SUPPLIES `next_run_time' for those two, pinning them to 2099 so they # are unambiguously not due, and lets exactly one job -- a one-shot whose run # date is in the past by construction -- be the thing that fires. # # The one thing read off the machine is the IANA database and the two files that # name this image's zone, which are configuration rather than a clock: # `localzone-*' below. # # ============================================================================ # THREADS DO NOT RUN ON THIS ARM, AND IT IS NOT THIS PORT'S DEFECT. # ============================================================================ # # pthread_create does not produce a running thread here # (linux-in-the-browser#85): the module imports, locks and thread-locals work, # and a started thread never runs its body -- which `99-python3's own # `thread-start-refused' already asserts. `BackgroundScheduler.start()' creates # a thread for its main loop, so the threaded scheduler cannot work on this # image, and no arrangement of this suite would make it. # # What that DOES leave reachable is the entire scheduling path, because # `BaseScheduler' documents a subclass supplying `wakeup()' as the way to write # a custom scheduler, and `DebugExecutor' runs a job synchronously in the # calling thread. The probe drives `_process_jobs()' -- the same call a real # scheduler's main loop makes -- over a `MemoryJobStore', so `sched-*' below # measures jobs being added, coming due, FIRING, and removing themselves, with # no thread created anywhere. # # The threaded path is measured too, as a witness, in a SEPARATE python3 start. # Separate because a `start()' that HANGS rather than raising would otherwise # take the whole transcript down with it: on its own it costs one case. # # ============================================================================ # THE MISFIRE GRACE TIME IS ONE SECOND, AND IT IS WHY A LATE JOB DOES NOTHING. # ============================================================================ # # `add_job' defaults `misfire_grace_time' to 1. A job whose run time has passed # by more than that is reported MISSED and its function is never called -- so a # scheduler that was stopped over the interesting instant silently runs nothing, # which is the first surprise anybody meets. `sched-missed-not-run' is the # control that pins it beside three jobs that pass `misfire_grace_time=None' and # do run. # # ============================================================================ # TWO ANSWERS THAT ARE THE OPPOSITE OF WHAT THE NAMES SUGGEST. # ============================================================================ # # `cron-uses-previous'. A cron trigger READS the previous fire time: it starts # its search at `min(now, previous + 1us)', so handing it one from January # answers with an instant in January -- BEFORE `now'. That is the misfire path, # not a bug, and it is measured because "cron ignores previous" is what the name # invites a reader to assume. # # `iv-no-catchup'. An interval trigger with a previous fire time adds exactly one # interval to it and does NOT skip forward to the present, so a scheduler that # stalled for four minutes replays rather than jumps. Catching up is `coalesce', # a JOB option, and not something the trigger does. # # ============================================================================ # DST: MEASURED, BECAUSE THE PLAUSIBLE ANSWERS ARE WRONG IN BOTH DIRECTIONS. # ============================================================================ # # America/New_York springs forward 2024-03-10, so the wall-clock hour 02:xx does # not exist that day. A daily 02:30 cron job is NOT skipped: the trigger builds # the naive 02:30 and localizes it at the offset in force BEFORE the transition, # giving `2024-03-10 02:30:00 EST-0500' -- an instant a wall clock in New York # shows as 03:30 EDT. The two answers a reader reaches for first are "no fire # that day" and "03:30 EDT", and it is neither as written. # # Falling back 2024-11-03 the hour 01:xx happens twice, and a daily 01:30 job # answers the SECOND one, EST-0500 -- an hour later in absolute time than the # EDT occurrence. # # The spring-forward is also where cron and interval part, which is the whole # reason both exist: `dst-interval-absolute' adds 24 real hours to 12:00 EST and # lands at 13:00 EDT, while `dst-cron-wallclock' stays at 12:00. # # ============================================================================ # WHAT IS NOT ASSERTED. # ============================================================================ # # Nothing sleeps and nothing waits on wall-clock time, so this suite makes no # statement about scheduler LATENCY on this arm. It cannot: the loop that would # produce a latency runs in a thread that does not run. P=python3-apscheduler bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/apscheduler DI=$SITE/APScheduler-3.10.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-apscheduler.list TZLIST=/var/lib/dpkg/info/python3-tzlocal.list PROBE=$BST_DATA/python3-apscheduler-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-apscheduler.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the shipped tree -- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/apscheduler.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/util-py test -s "$PKGDIR/util.py" check $P/job-py test -s "$PKGDIR/job.py" check $P/events-py test -s "$PKGDIR/events.py" check $P/triggers-dir test -d "$PKGDIR/triggers" check $P/triggers-date test -s "$PKGDIR/triggers/date.py" check $P/triggers-interval test -s "$PKGDIR/triggers/interval.py" check $P/triggers-cron-dir test -d "$PKGDIR/triggers/cron" check $P/triggers-cron-fields test -s "$PKGDIR/triggers/cron/fields.py" check $P/triggers-cron-expressions test -s "$PKGDIR/triggers/cron/expressions.py" check $P/schedulers-dir test -d "$PKGDIR/schedulers" check $P/schedulers-base test -s "$PKGDIR/schedulers/base.py" check $P/schedulers-background test -s "$PKGDIR/schedulers/background.py" check $P/jobstores-dir test -d "$PKGDIR/jobstores" check $P/jobstores-memory test -s "$PKGDIR/jobstores/memory.py" check $P/executors-dir test -d "$PKGDIR/executors" check $P/executors-pool test -s "$PKGDIR/executors/pool.py" check $P/executors-debug test -s "$PKGDIR/executors/debug.py" check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # The recipe declares no console script, and upstream ships none. check $P/no-cli test ! -e /usr/bin/apscheduler # ---------------------------------------------------------- the packaged files -- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON. This image's python3 is one static binary with an empty # lib-dynload and no dynamic loader (BRINTOS-1476), so a shared object in this # package could not be imported whatever it built to. 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" # Upstream's own test suite must not be packaged: the sdist keeps `tests/' at the # top level and setup.py's find_packages excludes it, so no `apscheduler.tests' # subpackage exists to ship. check $P/no-tests-subpackage test ! -d "$PKGDIR/tests" check_eq $P/pkg-no-test-files "" \ "$(grep -E '/apscheduler/tests/' "$W/pkgf" 2>/dev/null)" # ---------------------------------------------------------------- .dist-info ---- # NOT COSMETIC. BaseScheduler resolves every string trigger/executor/jobstore # alias through importlib.metadata at class-definition time, and # apscheduler/__init__.py reads its own version the same way. Without this # directory the package imports but every `trigger="cron"' becomes a KeyError # and __version__ silently reads 3.5.0. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-entry-points test -s "$DI/entry_points.txt" check_out $P/dist-info-version "Version: 3.10.4" cat "$DI/METADATA" check_out $P/dist-info-name "Name: APScheduler" cat "$DI/METADATA" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-top-level "apscheduler" cat "$DI/top_level.txt" check_out $P/entry-points-triggers "apscheduler.triggers" cat "$DI/entry_points.txt" # ------------------------------------------------------------------- dpkg ------ sed -n '/^Package: python3-apscheduler$/,/^$/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: 3.10.4" cat "$W/st" # THE DEPENDS LINE IS THIS PORT'S OWN WORK. meta-oe's recipe declares no # RDEPENDS at all, so as shipped the package reads `Depends: python3-core' and # cannot import itself: six, pytz and tzlocal are module-scope imports on the # default path. The bbappend in meta-brintos adds all eleven, and each entry # below is one the import closure actually reaches. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-six "python3-six" cat "$W/dep" check_out $P/pkg-depends-pytz "python3-pytz" cat "$W/dep" check_out $P/pkg-depends-tzlocal "python3-tzlocal" cat "$W/dep" check_out $P/pkg-depends-asyncio "python3-asyncio" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-logging "python3-logging" cat "$W/dep" check_out $P/pkg-depends-math "python3-math" cat "$W/dep" check_out $P/pkg-depends-netclient "python3-netclient" cat "$W/dep" check_out $P/pkg-depends-pickle "python3-pickle" cat "$W/dep" check_out $P/pkg-depends-threading "python3-threading" cat "$W/dep" # `python3-setuptools-scm-native' is a BUILD dependency that supplies the # version. A target Depends naming it would be the classic native/target leak. check_eq $P/pkg-no-setuptools-scm "" \ "$(grep -E 'setuptools-scm' "$W/dep" 2>/dev/null)" check_eq $P/no-ptest-package "" \ "$(sed -n '/^Package: python3-apscheduler-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # python3-tzlocal arrives on this image WITH this port and for no other reason, # so its own installation is part of what is being asserted here. cat "$TZLIST" > "$W/tzf" 2>/dev/null || : > "$W/tzf" check $P/tzlocal-installed test -s "$W/tzf" check $P/tzlocal-dir test -d "$SITE/tzlocal" check $P/tzlocal-unix-py test -s "$SITE/tzlocal/unix.py" # ------------------------------------------------------------- the local zone -- # CONFIGURATION, NOT A CLOCK. tzlocal resolves the zone from /etc/timezone and # the /etc/localtime symlink, both of which tzdata puts on this image, and # validates the name against /usr/share/zoneinfo. The three probe cases that # score the result are pinned to `Universal' because that is what these files # say -- so if one of them moved, these three shell cases say which. # # A `TZ' export in the harness environment would outrank all of it: tzlocal # checks the environment first. Nothing in brintos-selftest sets one. check $P/etc-timezone-file test -s /etc/timezone check_out $P/etc-timezone-universal "Universal" cat /etc/timezone check $P/etc-localtime test -e /etc/localtime check $P/zoneinfo-universal test -s /usr/share/zoneinfo/Universal check $P/zoneinfo-ny test -s /usr/share/zoneinfo/America/New_York check $P/zoneinfo-helsinki test -s /usr/share/zoneinfo/Europe/Helsinki # ------------------------------------------------------------------ imports ---- check $P/import python3 -c 'import apscheduler' # A clean import writes NOTHING to stderr. A DeprecationWarning from a stdlib # call the package makes would land here and nowhere else. check_eq $P/import-quiet "" \ "$(python3 -c 'import apscheduler' 2>&1)" # All seven submodules in one interpreter start rather than seven. check $P/import-submodules python3 -c 'import apscheduler.schedulers.base, apscheduler.triggers.cron, apscheduler.triggers.interval, apscheduler.triggers.date, apscheduler.jobstores.memory, apscheduler.executors.debug, apscheduler.job, apscheduler.util, apscheduler.events' # ------------------------------------------------------------------- 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 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 # The probe silences apscheduler's logger precisely so that this is assertable. check_eq $P/probe-quiet "" "$(cat "$OUT".err 2>/dev/null)" check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "130" "$(pv emitted_count)" # ---------------------------------------------------------------- identity ----- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # `version_attr' is read from the .dist-info at import; `version_is_fallback' # separates a genuinely wrong version from a missing .dist-info, which is the # same absence that would empty the alias tables below. check_eq $P/version-attr "3.10.4" "$(pv version_attr)" check_eq $P/version-info "3.10.4" "$(pv version_info)" check_eq $P/dist-version "3.10.4" "$(pv dist_version)" check_eq $P/version-not-fallback "no" "$(pv version_is_fallback)" check_eq $P/api-schedulers "yes" "$(pv has_schedulers)" check_eq $P/api-triggers "yes" "$(pv has_triggers)" check_eq $P/api-jobstores "yes" "$(pv has_jobstores)" check_eq $P/api-executors "yes" "$(pv has_executors)" check_eq $P/api-job "yes" "$(pv has_job)" check_eq $P/api-util "yes" "$(pv has_util)" check_eq $P/api-events "yes" "$(pv has_events)" # ------------------------------------------------------------- alias tables ---- check_eq $P/aliases-triggers "and,cron,date,interval,or" "$(pv trigger_aliases)" check_eq $P/aliases-executors "asyncio,debug,gevent,processpool,threadpool,tornado,twisted" "$(pv executor_aliases)" check_eq $P/aliases-jobstores "memory,mongodb,redis,rethinkdb,sqlalchemy,zookeeper" "$(pv jobstore_aliases)" # ------------------------------------------------------------ the port's scope -- check_eq $P/optional-sqlalchemy-absent "absent" "$(pv opt_sqlalchemy)" check_eq $P/optional-pymongo-absent "absent" "$(pv opt_pymongo)" check_eq $P/optional-redis-absent "absent" "$(pv opt_redis)" check_eq $P/optional-rethinkdb-absent "absent" "$(pv opt_rethinkdb)" check_eq $P/optional-twisted-absent "absent" "$(pv opt_twisted)" check_eq $P/optional-tornado-absent "absent" "$(pv opt_tornado)" check_eq $P/optional-gevent-absent "absent" "$(pv opt_gevent)" check_eq $P/optional-kazoo-absent "absent" "$(pv opt_kazoo)" # Loading the alias is what reaches the missing distribution, and each of these # three fails at a DIFFERENT point in the import, which is why the expectations # are not identical. check_eq $P/jobstore-sqlalchemy-refused "ImportError" "$(pv jobstore_load_sqlalchemy)" check_eq $P/jobstore-mongodb-refused "ImportError" "$(pv jobstore_load_mongodb)" check_eq $P/jobstore-redis-refused "ImportError" "$(pv jobstore_load_redis)" check_eq $P/executor-tornado-refused "ModuleNotFoundError" "$(pv executor_load_tornado)" check_eq $P/executor-gevent-refused "ImportError" "$(pv executor_load_gevent)" # THE ODD ONE OUT, and measured rather than assumed to match its neighbours: # apscheduler/executors/twisted.py imports nothing from twisted at module level, # so its alias LOADS on an image with no twisted. It would fail at construction, # not here. check_eq $P/executor-twisted-loads "TwistedExecutor" "$(pv executor_load_twisted)" # What the port DOES ship, resolved through the same table. check_eq $P/jobstore-memory "MemoryJobStore" "$(pv jobstore_load_memory)" check_eq $P/executor-debug "DebugExecutor" "$(pv executor_load_debug)" check_eq $P/executor-threadpool "ThreadPoolExecutor" "$(pv executor_load_threadpool)" check_eq $P/executor-processpool "ProcessPoolExecutor" "$(pv executor_load_processpool)" check_eq $P/executor-asyncio "AsyncIOExecutor" "$(pv executor_load_asyncio)" check_eq $P/trigger-date "DateTrigger" "$(pv trigger_load_date)" check_eq $P/trigger-interval "IntervalTrigger" "$(pv trigger_load_interval)" check_eq $P/trigger-cron "CronTrigger" "$(pv trigger_load_cron)" check_eq $P/trigger-and "AndTrigger" "$(pv trigger_load_and)" check_eq $P/trigger-or "OrTrigger" "$(pv trigger_load_or)" # ------------------------------------------------------------- cron triggers --- check_eq $P/cron-minute-top "2024-03-15 14:00:00 UTC+0000" "$(pv cron_minute_top)" # `now' exactly ON a matching instant fires at `now', it does not roll forward. check_eq $P/cron-on-the-boundary "2024-03-15 14:00:00 UTC+0000" "$(pv cron_on_the_boundary)" check_eq $P/cron-hour-minute "2024-03-16 03:30:00 UTC+0000" "$(pv cron_hour_minute)" # See the header: previous is READ, and the answer is before `now'. check_eq $P/cron-uses-previous "2024-01-01 03:30:00 UTC+0000" "$(pv cron_uses_previous)" # Ranges are inclusive at BOTH ends: 17:00 fires, 18:00 rolls to the next day. check_eq $P/cron-range-before "2024-03-15 09:00:00 UTC+0000" "$(pv cron_range_before)" check_eq $P/cron-range-inside "2024-03-15 13:00:00 UTC+0000" "$(pv cron_range_inside)" check_eq $P/cron-range-last "2024-03-15 17:00:00 UTC+0000" "$(pv cron_range_last)" check_eq $P/cron-range-after "2024-03-16 09:00:00 UTC+0000" "$(pv cron_range_after)" # Steps are anchored at the field's minimum, not at `now'. check_eq $P/cron-step-quarter "2024-03-15 13:45:00 UTC+0000" "$(pv cron_step_quarter)" check_eq $P/cron-step-wraps "2024-03-15 14:00:00 UTC+0000" "$(pv cron_step_wraps)" check_eq $P/cron-step-in-range "2024-03-15 13:00:00 UTC+0000" "$(pv cron_step_in_range)" check_eq $P/cron-list "2024-03-15 14:00:00 UTC+0000" "$(pv cron_list)" check_eq $P/cron-list-wraps "2024-03-15 14:00:00 UTC+0000" "$(pv cron_list_wraps)" check_eq $P/cron-list-of-ranges "2024-03-15 20:00:00 UTC+0000" "$(pv cron_list_of_ranges)" # 2024-03-15 is a Friday. `0' is Monday in APScheduler, not Sunday -- which is # where a crontab habit misfires by a day, so both spellings are pinned. check_eq $P/cron-dow-name "2024-03-18 00:00:00 UTC+0000" "$(pv cron_dow_name)" check_eq $P/cron-dow-range "2024-03-18 06:00:00 UTC+0000" "$(pv cron_dow_range)" check_eq $P/cron-dow-number "2024-03-18 00:00:00 UTC+0000" "$(pv cron_dow_number)" check_eq $P/cron-dow-name-is-number "$(pv cron_dow_name)" "$(pv cron_dow_number)" # `last' is the month's own length -- 29 in February 2024, 28 in 2023, 30 in # April -- which is how "the last day of the month" is written without computing # it. `last fri' and `3rd fri' are the ordinal forms. check_eq $P/cron-last-dom-leap "2024-02-29 00:00:00 UTC+0000" "$(pv cron_last_dom_leap)" check_eq $P/cron-last-dom-nonleap "2023-02-28 00:00:00 UTC+0000" "$(pv cron_last_dom_nonleap)" check_eq $P/cron-last-dom-30 "2024-04-30 00:00:00 UTC+0000" "$(pv cron_last_dom_30)" check_eq $P/cron-last-friday "2024-03-29 00:00:00 UTC+0000" "$(pv cron_last_friday)" check_eq $P/cron-3rd-friday "2024-03-15 00:00:00 UTC+0000" "$(pv cron_3rd_friday)" check_eq $P/cron-month-step "2024-04-01 00:00:00 UTC+0000" "$(pv cron_month_step)" # `week' is the ISO week number: week 10 of 2024 begins Monday 4 March. check_eq $P/cron-week "2024-03-04 00:00:00 UTC+0000" "$(pv cron_week)" check_eq $P/cron-year "2025-01-01 00:00:00 UTC+0000" "$(pv cron_year)" # A bound that has passed is `None', not an exception -- the scheduler reads # that as "this job is finished" and removes it. check_eq $P/cron-end-date "None" "$(pv cron_end_date)" check_eq $P/cron-start-date "2024-06-01 00:00:00 UTC+0000" "$(pv cron_start_date)" check_eq $P/cron-from-crontab "2024-03-18 09:00:00 UTC+0000" "$(pv cron_from_crontab)" check_eq $P/cron-repr "cron[hour='3', minute='30']" "$(pv cron_repr)" # An out-of-range field and an unparseable expression are both refused at # CONSTRUCTION, so a typo is a startup error rather than a job that never fires. check_eq $P/cron-bad-field "ValueError" "$(pv cron_bad_field)" check_eq $P/cron-bad-expr "ValueError" "$(pv cron_bad_expr)" # --------------------------------------------------------- interval triggers --- check_eq $P/iv-first-is-start "2024-03-15 12:00:00 UTC+0000" "$(pv iv_first_is_start)" check_eq $P/iv-from-previous "2024-03-15 12:00:30 UTC+0000" "$(pv iv_from_previous)" # See the header: no catch-up. `now' is four minutes past and the answer is not. check_eq $P/iv-no-catchup "2024-03-15 12:00:30 UTC+0000" "$(pv iv_no_catchup)" check_eq $P/iv-no-catchup-equals-plus-one "$(pv iv_from_previous)" "$(pv iv_no_catchup)" check_eq $P/iv-minutes "2024-03-15 12:05:00 UTC+0000" "$(pv iv_minutes)" check_eq $P/iv-mixed-units "2024-03-15 13:30:00 UTC+0000" "$(pv iv_mixed_units)" check_eq $P/iv-end-date "None" "$(pv iv_end_date)" check_eq $P/iv-weeks-days "2024-03-23 12:00:00 UTC+0000" "$(pv iv_weeks_days)" check_eq $P/iv-repr "interval[0:00:30]" "$(pv iv_repr)" # ------------------------------------------------------------- date triggers --- # A one-shot in the past still ANSWERS with its run date: whether a late job runs # is the scheduler's misfire decision, not the trigger's. check_eq $P/dt-past "2024-01-01 00:00:00 UTC+0000" "$(pv dt_past)" check_eq $P/dt-future "2024-06-01 00:00:00 UTC+0000" "$(pv dt_future)" check_eq $P/dt-fires-once "None" "$(pv dt_fires_once)" check_eq $P/dt-from-string "2024-06-01 08:30:00 UTC+0000" "$(pv dt_from_string)" check_eq $P/dt-repr "date[2024-06-01 00:00:00 UTC]" "$(pv dt_repr)" # ---------------------------------------------------------------------- DST ---- # See the header. These are the cases a caller gets wrong by adding 24 hours. check_eq $P/dst-spring-gap "2024-03-10 02:30:00 EST-0500" "$(pv dst_spring_gap)" check_eq $P/dst-spring-before "2024-03-10 01:30:00 EST-0500" "$(pv dst_spring_before)" check_eq $P/dst-spring-after "2024-03-11 02:30:00 EDT-0400" "$(pv dst_spring_after)" check_eq $P/dst-fall-ambiguous "2024-11-03 01:30:00 EST-0500" "$(pv dst_fall_ambiguous)" check_eq $P/dst-fall-after "2024-11-04 01:30:00 EST-0500" "$(pv dst_fall_after)" check_eq $P/dst-interval-absolute "2024-03-10 13:00:00 EDT-0400" "$(pv dst_interval_absolute)" check_eq $P/dst-cron-wallclock "2024-03-10 12:00:00 EDT-0400" "$(pv dst_cron_wallclock)" # A second zone, so nothing above is an accident of one zone's rules: Helsinki # springs forward 2024-03-31 at 03:00 EET, and a 03:30 job lands at 03:30 EET. check_eq $P/dst-helsinki "2024-03-31 03:30:00 EET+0200" "$(pv dst_helsinki)" # ------------------------------------- the scheduler, with no thread anywhere --- check_eq $P/sched-state-stopped "True" "$(pv sched_state_stopped)" check_eq $P/sched-start "True" "$(pv sched_start)" check_eq $P/sched-running "True" "$(pv sched_running)" # add_job through the STRING aliases -- the same table the .dist-info feeds. check_eq $P/sched-add-date "j-date" "$(pv sched_add_date)" check_eq $P/sched-add-interval "j-interval" "$(pv sched_add_interval)" check_eq $P/sched-add-cron "j-cron" "$(pv sched_add_cron)" check_eq $P/sched-add-missed "j-missed" "$(pv sched_add_missed)" check_eq $P/sched-jobs-count "4" "$(pv sched_jobs_count)" check_eq $P/sched-job-ids "j-cron,j-date,j-interval,j-missed" "$(pv sched_job_ids)" check_eq $P/sched-get-job "j-date" "$(pv sched_get_job)" check_eq $P/sched-get-missing "None" "$(pv sched_get_missing)" # A duplicate id is refused rather than silently replacing the job; the caller # has to say `replace_existing' to mean it. check_eq $P/sched-conflicting-id "ConflictingIdError" "$(pv sched_conflicting_id)" check_eq $P/sched-replace-existing "j-date" "$(pv sched_replace_existing)" # THE JOB FIRES. One pass of the same call a real scheduler's main loop makes # runs the one due job, in the calling thread, exactly once. check_eq $P/sched-process-1 "date" "$(pv sched_process_1)" check_eq $P/sched-ran-once "1" "$(pv sched_ran_count)" # ... and the one whose run time is older than its misfire grace time does not. check_eq $P/sched-missed-not-run "yes" "$(pv sched_missed_not_run)" # A second pass adds nothing: the one-shots removed themselves and the recurring # two are not due. No job fires twice for one due instant. check_eq $P/sched-process-2 "date" "$(pv sched_process_2)" check_eq $P/sched-no-double-fire "$(pv sched_process_1)" "$(pv sched_process_2)" check_eq $P/sched-date-job-gone "None" "$(pv sched_date_job_gone)" check_eq $P/sched-jobs-after-run "j-cron,j-interval" "$(pv sched_jobs_after_run)" check_eq $P/sched-remove "j-interval" "$(pv sched_remove)" check_eq $P/sched-remove-missing "JobLookupError" "$(pv sched_remove_missing)" # Pausing clears next_run_time and KEEPS the job; resuming recomputes it. check_eq $P/sched-pause-clears-next "None" "$(pv sched_pause_clears_next)" check_eq $P/sched-paused-job-kept "j-interval" "$(pv sched_paused_job_kept)" check_eq $P/sched-resume-restores-next "set" "$(pv sched_resume_restores_next)" check_eq $P/sched-remove-all "0" "$(pv sched_remove_all)" check_eq $P/sched-shutdown "True" "$(pv sched_shutdown)" check_eq $P/sched-shutdown-twice "SchedulerNotRunningError" "$(pv sched_shutdown_twice)" # ------------------------------------------------------------- the local zone -- # Pinned to what this image's own /etc/timezone and /etc/localtime say, asserted # as files by `etc-timezone-universal' above. `Universal' is an IANA link to UTC. check_eq $P/localzone-type "ZoneInfo" "$(pv localzone_type)" check_eq $P/localzone-key "Universal" "$(pv localzone_key)" # A scheduler constructed with no `timezone=' adopts it, which is the one place # the default path reads the machine at all. check_eq $P/localzone-is-scheduler-default "Universal" "$(pv default_tz_scheduler)" # ---------------------------------------------------------------- closure ------ # EXACTLY THE THREE DISTRIBUTIONS `install_requires' NAMES, and nothing else. check_eq $P/pulled-nonstdlib "pytz,six,tzlocal" "$(pv pulled_nonstdlib)" check_eq $P/needs-six "yes" "$(pv needs_six)" check_eq $P/needs-pytz "yes" "$(pv needs_pytz)" check_eq $P/needs-tzlocal "yes" "$(pv needs_tzlocal)" # `from asyncio import iscoroutinefunction' in util.py, on the default path -- # which is why python3-asyncio is on the Depends line of a package that # schedules nothing asynchronously. check_eq $P/needs-asyncio "yes" "$(pv needs_asyncio)" check_eq $P/tests-not-imported "" "$(pv tests_not_imported)" # ------------------------------------------------------- the threads boundary -- # A SEPARATE python3 start, deliberately: if `BackgroundScheduler.start()' HANGS # rather than raising, this costs one case instead of the whole transcript above. # # pthread_create does not produce a running thread on this arm # (linux-in-the-browser#85), so the threaded scheduler cannot work here. Both # cases are scored on the NEGATIVE -- "the thread did not run", "the scheduler # did not report itself started" -- rather than on an exception type, because the # type is the platform's to choose and only the outcome is this port's business. # The transcript still carries the type, for whoever reads it next. # # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#85 revisit -- when a started # thread runs its body, BackgroundScheduler works on this image and these two # cases invert. They are the signal that says so. bst_probe $P/threads-probe-run "$W/th" 300 python3 -c ' import threading done = [] t = threading.Thread(target=lambda: done.append(1)) try: t.start() except Exception as e: print("THREAD:REFUSED:" + type(e).__name__) else: t.join(5) print("THREAD:RAN" if done else "THREAD:REFUSED:silent") from apscheduler.schedulers.background import BackgroundScheduler s = BackgroundScheduler() try: s.start() except Exception as e: print("BG:REFUSED:" + type(e).__name__) else: print("BG:STARTED") s.shutdown(wait=False) ' check_eq $P/thread-does-not-run "" \ "$(grep '^THREAD:RAN' "$W/th" 2>/dev/null)" check_out $P/thread-refused "THREAD:REFUSED" cat "$W/th" check_eq $P/background-scheduler-refused "" \ "$(grep '^BG:STARTED' "$W/th" 2>/dev/null)" check_out $P/background-scheduler-reason "BG:REFUSED" cat "$W/th" rm -rf "$W"