. "$BST_LIB" # cronie is the second DAEMON on this image (sysklogd was the first) and the only # port whose whole purpose is to act at a particular WALL-CLOCK time. That shapes # this suite in three ways. # # 1. Waits are bounded in WALL CLOCK, never in iterations. sleep(1) is unreliable # here -- the relative-timer path, linux-in-the-browser#120. Measured on this # image: 30 consecutive `sleep 1` took 31 s (it did sleep), while a 40-iteration # `sleep 1` poll in another run finished in 0 s of wall clock (it did not), and # `sleep 3` returned after 0.22 s. So an iteration count says nothing about how # long a wait lasted, and 86-sysklogd's assumption that one poll costs about a # second does not hold. The waits below count seconds off date(1), which is the # one clock that has proven trustworthy: it tracks real UTC and its seconds # advance correctly. That is also why cron works here at all. # # 2. Everything that can be asserted without the daemon is asserted BEFORE it, and # the skips come before it too. Only the last block can spend wall clock, so a # timeout there cannot convert the other twenty results into one suite-timeout # red -- the failure mode that cost 86-sysklogd eleven results on its first # draft. # # 3. Every daemon case passes -p. crond's default sanity checks reject a crontab # whose st_uid is not root's, and this rootfs reports the HOST's uid rather than # 0 (`id -u` says 0 while `stat /etc/passwd` says 1008), so unpatched checks log # WRONG FILE OWNER for /etc/crontab and /var/spool/cron/root and the database # loads empty. -p is upstream's own flag for "do not apply those checks", and it # is what makes these cases measure cronie instead of the filesystem's uid # reporting. cronie/owner-checks records that rather than hiding it. require crond cronie/present D=/tmp/.bst-cronie CONF=$D/syslog.conf LOG=$D/syslog.log OUT=$D/fired CT=$D/ct export HOME=$D/home # wait_until -- poll a condition, bounded by REAL # seconds. Returns 1 on timeout so the caller reports the condition, not a hang. wait_until() { _lim=$1; shift _t0=$(date -u +%s) while [ $(( $(date -u +%s) - _t0 )) -lt "$_lim" ]; do if "$@"; then return 0; fi sleep 1 done return 1 } logged() { grep -q "$1" "$LOG" 2>/dev/null; } nonempty() { [ -s "$1" ]; } rm -rf "$D"; mkdir -p "$D" "$HOME" # --- identity ---------------------------------------------------------------- # One source package, three binaries, one version string between them. check_out cronie/version "cronie 1.7.2" crond -V check_out cronie/crontab-version "cronie 1.7.2" crontab -V check_out cronie/cronnext-version "cronie 1.7.2" cronnext -V # --- inventory --------------------------------------------------------------- # Every path oe-core's recipe installs. crond lands in sbindir (/usr/sbin), which # is what lets the next case be written as an absence check. inv="" for p in /usr/sbin/crond /usr/bin/crontab /usr/bin/cronnext \ /etc/crontab /etc/init.d/crond /etc/sysconfig/crond /etc/cron.deny; do [ -f "$p" ] || inv="$inv $p" done for p in /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly \ /etc/cron.monthly /var/spool/cron; do [ -d "$p" ] || inv="$inv $p" done if [ -z "$inv" ]; then bst_ok cronie/inventory else bst_notok cronie/inventory "missing:$inv"; fi # busybox here is built with neither CROND nor CRONTAB, so cronie is the sole # provider of either name and nothing had to be excluded to make room for it -- # unlike sysklogd, which needed busybox-syslog dropped. Absence is the only # falsifiable form of that claim. if [ ! -e /sbin/crond ] && [ ! -e /sbin/crontab ] && [ "$(command -v crond)" = /usr/sbin/crond ] && [ "$(command -v crontab)" = /usr/bin/crontab ]; then bst_ok cronie/sole-provider else bst_notok cronie/sole-provider "crond=$(command -v crond) crontab=$(command -v crontab) /sbin: $(ls -d /sbin/cron* 2>&1)" fi # The "crontab" system group, created in the image's /etc/group by shadow-native # at rootfs assembly. It is the only thing cronie inherits useradd for, and its # presence is what shows that dropping the unbuildable TARGET shadow dependency # did not cost the group. check_out cronie/group "crontab:" grep '^crontab:' /etc/group # crontab(1) ships WITHOUT the setgid bit here -- a deliberate divergence from # upstream, because setgid makes egid differ from gid and this kernel cannot honour # the resulting credential change (setgid/setre*id are absent from its syscall # table), which would leave root unable to run crontab at all. This case cannot # fail under the local functional verify, whose rootfs is a plain host directory # that has already lost every set-id bit; it guards the recipe on a mode-preserving # deploy. What proves the divergence works HERE is cronie/install-list below -- # exactly the call that failed before it. if [ -u /usr/bin/crontab ] || [ -g /usr/bin/crontab ]; then bst_notok cronie/crontab-plain "still set-id: $(ls -l /usr/bin/crontab)" else bst_ok cronie/crontab-plain fi # The shipped /etc/crontab has every run-parts line commented out, so a crond # started on a stock image runs nothing until an operator adds something. Worth # pinning: it is the difference between this port being inert and it executing four # run-parts trees nobody asked for. if [ -z "$(grep -E '^[[:space:]]*[0-9*]' /etc/crontab)" ]; then bst_ok cronie/stock-inert else bst_notok cronie/stock-inert "active entries: $(_bst_1line "$(grep -E '^[[:space:]]*[0-9*]' /etc/crontab)")"; fi # --- option contracts -------------------------------------------------------- # Unlike sysklogd, whose bad flag exits 0 and reads as a clean run, both cronie # front ends report a bad option honestly. Pinned so a version that stops doing so # is caught rather than assumed. out=$(crond -Z 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "invalid option"; }; then bst_ok cronie/crond-badopt else bst_notok cronie/crond-badopt "rc=$rc $(_bst_1line "$out")"; fi out=$(crontab -Z 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "invalid option"; }; then bst_ok cronie/crontab-badopt else bst_notok cronie/crontab-badopt "rc=$rc $(_bst_1line "$out")"; fi # No crontab installed is rc=1, not rc=0, so a caller branching on the status can # tell "none" from "listed". Clear one out first: /var/spool/cron survives reboots # and an operator (or an earlier run of this suite) may have left one there, and # this case asserts the empty state rather than assuming it. crontab -r >/dev/null 2>&1 out=$(crontab -l 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "no crontab for"; }; then bst_ok cronie/list-empty else bst_notok cronie/list-empty "rc=$rc $(_bst_1line "$out")"; fi # --- the parser --------------------------------------------------------------- printf 'this is not a crontab line\n' > "$D/bad" out=$(crontab "$D/bad" 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "bad minute"; }; then bst_ok cronie/reject-garbage else bst_notok cronie/reject-garbage "rc=$rc $(_bst_1line "$out")"; fi printf '99 * * * * echo x\n' > "$D/bad2" out=$(crontab "$D/bad2" 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "bad minute"; }; then bst_ok cronie/reject-range else bst_notok cronie/reject-range "rc=$rc $(_bst_1line "$out")"; fi # --- crontab round trip ------------------------------------------------------- printf '@reboot echo FIRED >> %s\n' "$OUT" > "$CT" printf '30 4 * * * echo daily\n' >> "$CT" check cronie/install-list crontab "$CT" check_out cronie/list-back "@reboot echo FIRED" crontab -l # Reinstalling saves the previous crontab under $HOME/.cache/crontab -- a path that # has to be created TWO levels deep on a home directory that has never cached # anything, which upstream's single mkdir(2) cannot do; and whose error path then # restored an unassigned umask, undefined behaviour this toolchain turns into an # illegal instruction. Both are fixed in this port's patch 2. Before it, every # crontab install and every crontab -r trapped, so this case is that patch. crontab "$CT" >/dev/null 2>&1 if [ -s "$HOME/.cache/crontab/crontab.bak" ]; then bst_ok cronie/backup else bst_notok cronie/backup "no backup at $HOME/.cache/crontab/crontab.bak: $(_bst_1line "$(ls -la "$HOME/.cache" 2>&1)")"; fi # cronnext computes the next run time from a crontab with no daemon and no waiting, # so it is the one part of the schedule that can be asserted EXACTLY: 30 4 * * * # after 2001-09-09T01:46:40Z (1000000000) is 2001-09-09T04:30:00Z. printf '30 4 * * * echo daily\n' > "$D/nextsrc" check_out cronie/cronnext-next "next: 1000009800" cronnext -t 1000000000 "$D/nextsrc" check_out cronie/cronnext-list "nextjobs:" cronnext -t 1000000000 -l "$D/nextsrc" # --- removal (before the daemon block, so a timeout there cannot lose it) ------ check cronie/remove crontab -r out=$(crontab -l 2>&1); rc=$? if [ "$rc" -eq 1 ] && { echo "$out" | grep -q "no crontab for"; }; then bst_ok cronie/remove-gone else bst_notok cronie/remove-gone "rc=$rc $(_bst_1line "$out")"; fi # --- what this image cannot reach (also before the daemon block) --------------- # Each of these flips on its own if its cause is removed; none needs the suite # edited to stay honest. bst_skip cronie/minute-schedule "a * * * * entry needs up to 60 s of wall clock plus job-exec latency, i.e. the runner's entire per-suite budget; measured working by hand -- the job fired at exactly HH:MM:00, so cron_sleep's time(2) correction survives the unreliable sleep -- and cronnext-next asserts the same arithmetic here" bst_skip cronie/owner-checks "crond's default st_uid checks cannot pass: this rootfs reports the host's uid (stat says 1008) where the process is uid 0, so /etc/crontab and /var/spool/cron/root both log WRONG FILE OWNER. Every daemon case below uses -p. Not a cronie defect and not fixable in cronie" bst_skip cronie/crontab-edit "crontab -e needs an interactive EDITOR on a pty; no editor on this image" bst_skip cronie/mail "job output is mailed through a sendmail this image has no MTA for; every daemon case runs with -m off" bst_skip cronie/anacron "not built -- the recipe's PACKAGECONFIG leaves --disable-anacron, so there is no anacron binary to cover" bst_skip cronie/pam "not built -- pam is not in DISTRO_FEATURES, so crond has no PAM session to open" # --- the daemon --------------------------------------------------------------- # syslog is the ONLY channel crond reports on: it keeps no foreground log, so # without a listener its STARTUP banner, its CMD lines and every error it hits are # discarded. Starting the image's own sysklogd first doubles as the check that the # two daemon ports work together. crontab "$CT" >/dev/null 2>&1 printf '*.*\t%s\n' "$LOG" > "$CONF" syslogd -f "$CONF" -C "$D/kmsg.seqno" -P "$D/syslogd.pid" wait_until 8 test -S /dev/log rm -f /run/cron.reboot /run/crond.pid "$OUT" crond -n -m off -p >>"$D/crond.stderr" 2>&1 & if wait_until 10 nonempty /run/crond.pid; then bst_ok cronie/daemon-pidfile else bst_notok cronie/daemon-pidfile "no /run/crond.pid within 10 s: $(_bst_1line "$(tail -3 "$D/crond.stderr" 2>&1)")"; fi # The execution proof, and the reason this suite exists: an @reboot entry runs at # daemon start, so a job can be watched running without waiting out a minute # boundary. Measured latency was 2 s, 2 s and 3 s over three runs; 20 s is that # with room for a loaded host, and is the ONLY long wait in the suite. if wait_until 20 nonempty "$OUT"; then bst_ok cronie/reboot-job else bst_notok cronie/reboot-job "@reboot job produced nothing in 20 s: $(_bst_1line "$(grep -i cron "$LOG" 2>&1 | tail -3)")"; fi # crond announced itself, and bracketed the job it ran: CMD when it dispatched the # fork+exec, CMDEND when it reaped it. That pair is what separates "cron ran # something" from "something wrote the file". if wait_until 8 logged "(CRON) STARTUP (1.7.2)"; then bst_ok cronie/syslog-startup else bst_notok cronie/syslog-startup "no STARTUP in $LOG: $(_bst_1line "$(tail -3 "$LOG" 2>&1)")"; fi if wait_until 8 logged "CMDEND (echo FIRED" && logged "CMD (echo FIRED"; then bst_ok cronie/job-logged else bst_notok cronie/job-logged "no CMD/CMDEND pair: $(_bst_1line "$(grep -i cron "$LOG" 2>&1 | tail -3)")"; fi # Shutdown cannot be covered, and the reason is not cronie's. Signal delivery does # not work on this runtime AT ALL: kill(2) reports success and nothing arrives. # Measured with cronie out of the picture -- a `sleep 300` blocked in nanosleep, a # shell busy loop that never enters a syscall, and a `cat` blocked on a fifo all # survive SIGTERM *and SIGKILL* for 20 s, and `sh -c 'trap ... TERM; kill -TERM $$'` # prints NOT-TRAPPED, so even a process signalling itself never runs its handler. # crond survived SIGTERM for 100 s across two minute boundaries and SIGKILL for 40 s. # A process that ignores SIGKILL is not a program behaviour, so there is nothing # here for cronie to get right and no version of this case that could go green. bst_skip cronie/daemon-stop "signal delivery is broken platform-wide: kill(2) succeeds and nothing is delivered, SIGKILL included. Reproduced without cronie -- sleep(1) in nanosleep, a shell busy loop and cat on a fifo all survive SIGTERM and SIGKILL, and kill -TERM \$\$ with a TERM trap prints NOT-TRAPPED. crond survived SIGTERM 100 s and SIGKILL 40 s" # @reboot fires once per BOOT, not once per daemon start: crond records that it has # run them in /run/cron.reboot and says so when it finds the lock. The crond above # is necessarily still running (it cannot be signalled), so clear the pidfile and # let this one take the lock -- what is under test is the reboot lock, not the pid # lock. rm -f /run/crond.pid "$OUT" crond -n -m off -p >>"$D/crond.stderr" 2>&1 & if wait_until 15 logged "@reboot jobs will be run at computer.s startup"; then bst_ok cronie/reboot-once else bst_notok cronie/reboot-once "no lock notice: $(_bst_1line "$(grep -i reboot "$LOG" 2>&1 | tail -3)")"; fi crontab -r >/dev/null 2>&1 rm -rf "$D"