. "$BST_LIB" require rsnapshot rsnapshot/present # BST-TIMEOUT: 600 # ~9 rsnapshot runs, each forking rsync over a small tree # rsnapshot is rsync + hard links, and the hard links are the whole product. A # suite that only proved "the backup directory has my files in it" would pass # against a tool that re-copied every byte every rotation -- which is precisely # the thing rsnapshot exists not to do. So the load-bearing assertions here are # INODE IDENTITY across two snapshots, with the changed file asserted to be a # DIFFERENT inode in the same breath; either one alone is passable for the wrong # reason (link everything, or link nothing, would each satisfy half of it). # # Identity is st_ino equality, never st_nlink: the cloud store under-reports the # link count on the source of a link (brintos-fs PLAN-CROSS-PLATFORM.md ยง10), so # nlink would report a fabricated red here. 65-vfs.test settled that for link(2) # itself; this suite inherits the ruling. # # The whole fixture lives under $TMPDIR (tmpfs). The virtiofs root refuses # anything that is not a regular file, and rsnapshot's rotation creates # directories, symlinks and hard links -- so on the root this would measure the # filesystem, not the tool. # # No `sleep` anywhere: relative timers are broken here # (linux-in-the-browser#120), so nothing waits on wall-clock. rsnapshot needs no # timer of its own -- it is driven by an external scheduler (cron/at) in real # use, and each interval here is invoked directly. # # rsnapshot's config parser splits on TABS (rsnapshot-program.pl:574, # split(/\t+/)), which is why every line below is written with printf '\t' # rather than typed as whitespace -- a space-separated config parses as a single # unknown directive and configtest rejects the file. tmp=${TMPDIR:-/tmp} w=$tmp/bst-rsnapshot.$$ rm -rf "$w" SRC=$w/src mkdir -p "$SRC/sub" # --- 0. the port's own shape ------------------------------------------------ # rsnapshot is a perl script, so "did it build" is really "did configure # substitute the TARGET perl". A host-path shebang is the classic cross-build # leak and would make the tool unrunnable on a machine with no /usr/bin/perl at # that path -- it is asserted here because nothing else on the image would # notice it. RS=$(command -v rsnapshot 2>/dev/null) check_out rsnapshot/shebang "/usr/bin/perl" head -n 1 "$RS" check rsnapshot/diff-present command -v rsnapshot-diff check rsnapshot/conf-shipped test -f /etc/rsnapshot.conf.default # rsync is a hard RDEPENDS, not an optional accelerator: with no cmd_rsync there # is no code path that copies anything. Absent, it is this port that is broken, # so it is `not ok` and not a skip. RSYNC=$(command -v rsync 2>/dev/null) if [ -z "$RSYNC" ]; then bst_notok rsnapshot/rsync-present "rsync absent; rsnapshot RDEPENDS it and cannot copy without it" rm -rf "$w" exit 0 fi bst_ok rsnapshot/rsync-present # cmd_rsync is the one directive rsnapshot cannot run without, and the shipped # /etc/rsnapshot.conf.default is where an operator gets it from. meta-oe's # configure-fix-cmd_rsync.patch writes that line as `cmd_rsync ${exec_prefix}/ # bin/rsync` -- two SPACES where the parser splits on tabs, and a variable # configure never expands -- so the file we ship would be rejected by the very # tool it configures. The bbappend pins ac_cv_path_RSYNC instead of passing # --without-rsync, which takes the unpatched code path; these two cases are that # fix's regression guard, and they read the file as installed, not as intended. TAB=$(printf '\t') cline=$(grep '^cmd_rsync' /etc/rsnapshot.conf.default 2>/dev/null) case "$cline" in *"$TAB"*) bst_ok rsnapshot/conf-cmd-tab ;; *) bst_notok rsnapshot/conf-cmd-tab "cmd_rsync is not tab-separated, so the shipped default does not parse: '$(_bst_1line "$cline")'" ;; esac case "$cline" in *"$RSYNC"*) bst_ok rsnapshot/conf-cmd-path ;; *) bst_notok rsnapshot/conf-cmd-path "want '$RSYNC', got '$(_bst_1line "$cline")'" ;; esac # --- 0a. can rsnapshot start? ----------------------------------------------- # Everything above reads files; everything below RUNS rsnapshot, and on this arm # it cannot run at all. rsnapshot is perl, and perl aborts in the glibc allocator # while COMPILING any core module -- `mremap_chunk(): invalid pointer`, or # `corrupted size vs. prev_size`, rc=127 -- before a line of rsnapshot's own code # executes (linux-in-the-browser#154). rsnapshot loads ten such modules, so # `version`, `configtest` and a real backup all die identically and produce # nothing. # # This is a guard, NOT a skip of an inconvenient case: the probe below is left # FAILING and attributed, so it flips green by itself when #154 closes, which is # the regression signal this suite is bought for. What the guard prevents is # running the rotation anyway -- that costs far more than the ~40 further reds it # looks like it would produce. Measured on this image: the suite emits # ==BST-BEGIN== and then NO case output at all while the host guest process grows # ~2400 MiB/min to a 3.7 GiB peak, until the harness quarantines the machine on # its RSS-rate bound. Every suite scheduled after this one in that sweep is lost, # so an unguarded rsnapshot is a hazard to the whole run rather than a red of its # own. vout=$(rsnapshot version 2>&1); vrc=$? case "$vout" in *rsnapshot*) bst_ok rsnapshot/version ;; *) bst_notok rsnapshot/version \ "rc=$vrc $(_bst_1line "$vout") -- perl cannot load its core modules on this arm (linux-in-the-browser#154), so rsnapshot never starts" # Named rather than silent: the ticket's headline criterion is that the # unchanged files across two snapshots are hard links to one inode, and it is # exactly what could not be measured here. A suite that simply stopped would # report six greens and read as a pass. bst_notok rsnapshot/rotation-unverified \ "not run: rsnapshot cannot start (linux-in-the-browser#154) -- the hard-link identity criterion is unverified on wasm32-hwjs" rm -rf "$w" exit 0 ;; esac # --- the fixture ------------------------------------------------------------ printf 'alpha\n' > "$SRC/a.txt" # this one changes between snapshots printf 'bravo\n' > "$SRC/b.txt" # this one never changes -> must be linked printf 'charlie\n' > "$SRC/sub/c.txt" # nested, also never changes printf 'noise\n' > "$SRC/skipme.log" # excluded by the config ln -s a.txt "$SRC/link" SNAP=$w/snap LOCK=$w/rsnapshot.pid CFG=$w/rsnapshot.conf mkcfg() { # $1 cfg, $2 snapshot_root, $3 lockfile, $4 non-empty => link_dest 1 { printf 'config_version\t1.2\n' printf 'snapshot_root\t%s\n' "$2" printf 'cmd_rsync\t%s\n' "$RSYNC" printf 'lockfile\t%s\n' "$3" printf 'retain\talpha\t3\n' printf 'verbose\t2\n' printf 'loglevel\t3\n' if [ -n "$4" ]; then printf 'link_dest\t1\n'; fi printf 'exclude\t%s\n' '*.log' printf 'backup\t%s/\tlive/\n' "$SRC" } > "$1" } mkcfg "$CFG" "$SNAP" "$LOCK" "" # rsnapshot's default rsync_long_args carry --relative, so a backup point # `backup /a/b/ live/` lands at /./live/a/b/. That is the # documented layout, and computing it here (rather than globbing for the file) # is what makes a layout change a visible failure instead of a silent pass. P0=$SNAP/alpha.0/live$SRC P1=$SNAP/alpha.1/live$SRC P2=$SNAP/alpha.2/live$SRC # --- 1. the config parser --------------------------------------------------- check_out rsnapshot/configtest "Syntax OK" rsnapshot -c "$CFG" configtest # The negative control for the case above. Without it, a configtest that # accepted literally anything would read as a pass. BAD=$w/bad.conf { printf 'config_version\t1.2\n'; printf 'snapshot_root\t%s\n' "$SNAP" printf 'no_such_directive\tvalue\n'; } > "$BAD" out=$(rsnapshot -c "$BAD" configtest 2>&1); rc=$? if [ "$rc" -ne 0 ]; then bst_ok rsnapshot/configtest-rejects else bst_notok rsnapshot/configtest-rejects "accepted a bogus directive: $(_bst_1line "$out")"; fi # --- 2. first snapshot ------------------------------------------------------ check rsnapshot/run1 rsnapshot -c "$CFG" alpha check rsnapshot/root-created test -d "$SNAP/alpha.0" if [ -f "$P0/a.txt" ]; then bst_ok rsnapshot/layout else bst_notok rsnapshot/layout "expected $P0/a.txt; tree has: $(_bst_1line "$(find "$SNAP" -type f 2>/dev/null | head -n 3)")"; fi check_eq rsnapshot/snap0-file "alpha" "$(cat "$P0/a.txt" 2>/dev/null)" check_eq rsnapshot/snap0-nested "charlie" "$(cat "$P0/sub/c.txt" 2>/dev/null)" check_eq rsnapshot/snap0-symlink "a.txt" "$(readlink "$P0/link" 2>/dev/null)" if [ -e "$P0/skipme.log" ]; then bst_notok rsnapshot/exclude "excluded *.log was captured" else bst_ok rsnapshot/exclude; fi # an exclude that dropped the whole tree would satisfy the case above check_eq rsnapshot/exclude-scoped "bravo" "$(cat "$P0/b.txt" 2>/dev/null)" # the lock is taken for the duration and released on a clean exit if [ -e "$LOCK" ]; then bst_notok rsnapshot/lock-released "lockfile survived a clean run" else bst_ok rsnapshot/lock-released; fi # --- 3. the rotation, and the hard links that are the point of the tool ----- printf 'alpha-changed\n' > "$SRC/a.txt" check rsnapshot/run2 rsnapshot -c "$CFG" alpha check rsnapshot/snap1-exists test -d "$SNAP/alpha.1" # the two generations hold the two versions -- this is what "a backup" means check_eq rsnapshot/rotate-old "alpha" "$(cat "$P1/a.txt" 2>/dev/null)" check_eq rsnapshot/rotate-new "alpha-changed" "$(cat "$P0/a.txt" 2>/dev/null)" ino() { stat -c %i "$1" 2>/dev/null; } # THE criterion: an untouched file occupies ONE inode across both snapshots. i0=$(ino "$P0/b.txt"); i1=$(ino "$P1/b.txt") if [ -n "$i0" ] && [ -n "$i1" ]; then check_eq rsnapshot/hardlink-unchanged "$i0" "$i1" else bst_notok rsnapshot/hardlink-unchanged "stat -c %i gave alpha.0='$i0' alpha.1='$i1'" fi i0=$(ino "$P0/sub/c.txt"); i1=$(ino "$P1/sub/c.txt") if [ -n "$i0" ] && [ -n "$i1" ]; then check_eq rsnapshot/hardlink-nested "$i0" "$i1" else bst_notok rsnapshot/hardlink-nested "stat -c %i gave alpha.0='$i0' alpha.1='$i1'" fi # ...and the negative control that makes the two above mean something: the file # that CHANGED must not share an inode, or rsnapshot would be handing back a # snapshot that mutates under the reader. i0=$(ino "$P0/a.txt"); i1=$(ino "$P1/a.txt") if [ -z "$i0" ] || [ -z "$i1" ]; then bst_notok rsnapshot/changed-unlinked "stat -c %i gave alpha.0='$i0' alpha.1='$i1'" elif [ "$i0" != "$i1" ]; then bst_ok rsnapshot/changed-unlinked else bst_notok rsnapshot/changed-unlinked "changed file shares inode $i0 with the previous snapshot"; fi # --- 4. rsnapshot-diff: a second, independent witness of the same inodes ---- # rsnapshot-diff compares st_ino (rsnapshot-diff.pl:186), so it re-derives the # hard-link structure from the filesystem rather than from anything this suite # arranged. A file it does NOT report is a file it found shared. out=$(rsnapshot-diff -v "$SNAP/alpha.1" "$SNAP/alpha.0" 2>&1); rc=$? if [ "$rc" -eq 0 ]; then bst_ok rsnapshot/diff-runs else bst_notok rsnapshot/diff-runs "rc=$rc $(_bst_1line "$out")"; fi check_out rsnapshot/diff-summary "were added" printf '%s\n' "$out" case "$out" in *"/a.txt"*) bst_ok rsnapshot/diff-names-changed ;; *) bst_notok rsnapshot/diff-names-changed "the changed file is absent from the diff: $(_bst_1line "$out")" ;; esac case "$out" in *"/b.txt"*) bst_notok rsnapshot/diff-omits-linked "an untouched, hard-linked file was reported as a difference" ;; *) bst_ok rsnapshot/diff-omits-linked ;; esac # --- 5. retention: the ring is bounded -------------------------------------- check rsnapshot/run3 rsnapshot -c "$CFG" alpha check rsnapshot/run4 rsnapshot -c "$CFG" alpha check rsnapshot/retain-depth test -d "$SNAP/alpha.2" if [ -e "$SNAP/alpha.3" ]; then bst_notok rsnapshot/retain-bound "retain alpha 3 kept a fourth generation" else bst_ok rsnapshot/retain-bound; fi # ...and the ring actually AGED rather than merely stopping at three. Four runs # against a 3-deep ring must have pushed run 1's content off the end, so the # oldest surviving generation holds run 2's "alpha-changed", not the original # "alpha". A ring that kept the first snapshot forever would satisfy # retain-bound above and still be wrong. check_eq rsnapshot/retain-aged "alpha-changed" "$(cat "$P2/a.txt" 2>/dev/null)" # --- 6. the lockfile protocol ---------------------------------------------- # rsnapshot serialises itself with an O_EXCL PID file and decides staleness with # kill(0,$pid) -- so these two cases are also the only place on this image where # a perl process-existence probe is asserted in both directions. printf '%s\n' "$$" > "$LOCK" # a PID that is definitely alive: this shell out=$(rsnapshot -c "$CFG" alpha 2>&1); rc=$? case "$out" in *"can not continue"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -ne 0 ] && [ "$hit" -eq 1 ]; then bst_ok rsnapshot/lock-live-refuses else bst_notok rsnapshot/lock-live-refuses "rc=$rc a live lock did not stop the run: $(_bst_1line "$out")"; fi rm -f "$LOCK" # a PID that is definitely dead: reaped before the lockfile is written sh -c 'exit 0' & dead=$! wait "$dead" 2>/dev/null printf '%s\n' "$dead" > "$LOCK" out=$(rsnapshot -c "$CFG" alpha 2>&1); rc=$? case "$out" in *"stale lockfile"*) hit=1 ;; *) hit=0 ;; esac # rsnapshot's exit convention is 0 clean / 1 fatal / 2 completed-with-warnings, # and reclaiming a stale lock raises a warning by design -- so the assertion is # "not fatal", not "zero". Demanding 0 here would red on correct behaviour. if [ "$rc" -ne 1 ] && [ "$hit" -eq 1 ]; then bst_ok rsnapshot/lock-stale-recovers else bst_notok rsnapshot/lock-stale-recovers "rc=$rc stale lock not reclaimed: $(_bst_1line "$out")"; fi # the run it recovered into must have produced a snapshot, not just a warning check_eq rsnapshot/lock-stale-run "alpha-changed" "$(cat "$P0/a.txt" 2>/dev/null)" rm -f "$LOCK" # --- 7. -t is a rehearsal, not a run --------------------------------------- DRY=$w/dry; DCFG=$w/dry.conf mkcfg "$DCFG" "$DRY" "$w/dry.pid" "" out=$(rsnapshot -t -c "$DCFG" alpha 2>&1); rc=$? if [ "$rc" -eq 0 ]; then bst_ok rsnapshot/dry-run-rc else bst_notok rsnapshot/dry-run-rc "rc=$rc $(_bst_1line "$out")"; fi check_out rsnapshot/dry-run-reports "$RSYNC" printf '%s\n' "$out" if [ -e "$DRY/alpha.0" ]; then bst_notok rsnapshot/dry-run-inert "-t created a real snapshot" else bst_ok rsnapshot/dry-run-inert; fi # --- 8. the other linking engine: rsync --link-dest ------------------------- # With link_dest enabled rsnapshot stops hard-linking the tree itself and hands # the job to rsync's --link-dest. Same product, different subsystem -- and the # one this image has never exercised, since 58-rsync.test does not cover it. LSNAP=$w/lsnap; LCFG=$w/link.conf mkcfg "$LCFG" "$LSNAP" "$w/link.pid" yes printf 'alpha\n' > "$SRC/a.txt" check rsnapshot/linkdest-run1 rsnapshot -c "$LCFG" alpha printf 'alpha-again\n' > "$SRC/a.txt" check rsnapshot/linkdest-run2 rsnapshot -c "$LCFG" alpha L0=$LSNAP/alpha.0/live$SRC L1=$LSNAP/alpha.1/live$SRC check_eq rsnapshot/linkdest-old "alpha" "$(cat "$L1/a.txt" 2>/dev/null)" check_eq rsnapshot/linkdest-new "alpha-again" "$(cat "$L0/a.txt" 2>/dev/null)" i0=$(ino "$L0/b.txt"); i1=$(ino "$L1/b.txt") if [ -n "$i0" ] && [ -n "$i1" ]; then check_eq rsnapshot/linkdest-hardlink "$i0" "$i1" else bst_notok rsnapshot/linkdest-hardlink "stat -c %i gave alpha.0='$i0' alpha.1='$i1'" fi i0=$(ino "$L0/a.txt"); i1=$(ino "$L1/a.txt") if [ -z "$i0" ] || [ -z "$i1" ]; then bst_notok rsnapshot/linkdest-changed "stat -c %i gave alpha.0='$i0' alpha.1='$i1'" elif [ "$i0" != "$i1" ]; then bst_ok rsnapshot/linkdest-changed else bst_notok rsnapshot/linkdest-changed "changed file shares inode $i0 under --link-dest"; fi rm -rf "$w"