Skip to content

DRY refactor plus pinned-output regression tests and CI#1

Open
oetiker wants to merge 4 commits into
mainfrom
refactor-dry
Open

DRY refactor plus pinned-output regression tests and CI#1
oetiker wants to merge 4 commits into
mainfrom
refactor-dry

Conversation

@oetiker

@oetiker oetiker commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Refactor the repeated patterns in zzclone into helper subs: transfer_cmd() (the send | mbuffer | receive pipeline existed 4×), snap_name() (5 scattered snapshot-name regexes in 3 spellings), read_lines() (4 hand-rolled pipe-open loops), list_snapshots() and get_guid(). The full-send branch now reuses the already-computed $dest_fileset instead of re-deriving the destination by string surgery (this also anchors the src-root substitution, fixing a latent quirk).
  • Add t/regression.t: runs zzclone against a canned fake zfs/ssh/sudo world (t/shims) across 14 option combinations and compares stdout, stderr and exit code with pinned expected output (t/expected). The fake dataset tree covers incremental, full-send, up-to-date, no-snapshot, resume-token and guid-mismatch paths, in local, remote-source and remote-destination flavours. UPDATE_EXPECTED=1 prove t/ regenerates the expected files after an intentional output change.
  • Add a GitHub Actions workflow running perl -c and prove t/ on pushes to main and on PRs.
  • README: document the previously missing -R/--rollback, -c/--chain and -v/--verbose options and add a Testing section.

Verification

The generated output is byte-identical before and after the refactor: the pre-refactor script passes the exact same pinned test suite (all 28 tests) as the refactored one. No dependencies beyond core Perl; prove ships with it.

🤖 Generated with Claude Code

oetiker and others added 4 commits July 3, 2026 11:16
Introduce transfer_cmd(), snap_name(), read_lines(), list_snapshots()
and get_guid() to replace the copy-pasted send/receive pipelines,
snapshot-name regexes, pipe-open loops and guid lookups. The full-send
branch now reuses the already-computed $dest_fileset instead of
re-deriving the destination path from the snapshot name (this also
anchors the src-root substitution). Generated output is unchanged --
verified byte-identical across all option combinations against a fake
zfs/ssh/sudo shim covering incremental, full-send, up-to-date,
no-snapshot, resume-token and guid-mismatch paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
t/regression.t runs zzclone against a fake zfs/ssh/sudo world (t/shims)
across all option combinations and compares the generated commands with
pinned expected output (t/expected), covering the incremental,
full-send, up-to-date, no-snapshot, resume-token and guid-mismatch
paths. Regenerate the expected files with UPDATE_EXPECTED=1 prove t/.

A GitHub Actions workflow runs perl -c and the suite on every push to
main and on pull requests. The README options list gains the
previously undocumented -R/--rollback, -c/--chain and -v/--verbose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field experience from a bulk home-directory migration: an ssh transport
that wedges silently (data stops flowing, no EOF, no error) hangs the
whole send/receive pipeline forever, and a snapshot rotation on the
source between the initial full send and the incremental catch-up
strands the destination without a common snapshot.

- Every emitted ssh command now uses BatchMode plus keepalives
  (ServerAliveInterval 15, ServerAliveCountMax 4) so a dead connection
  fails within a minute instead of blocking forever.
- New -W/--watchdog <sec> adds mbuffer -W so a stalled transfer aborts
  (leaving a resume token under --resume) instead of hanging.
- Under --rollback the fallback full send now receives with -F,
  overwriting a destination that shares no snapshot with the source,
  consistent with the option's discard-destination-state contract.
- Test shim ssh now skips client options; new watchdog and rollback-F
  regression cases (32 assertions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A full stream cannot be received into an existing dataset: zfs refuses
outright, and even receive -F refuses once the destination carries
snapshots -- exactly the state left behind when a destination's only
base snapshot is rotated away on the source (interrupted first run,
sync delayed past the rotation window). Such datasets wedge every
subsequent run.

With -F/--overwrite, zzclone emits 'zfs destroy -r <dest>' before the
fallback full send so the destination is recreated from scratch.
Without it the doomed full send is now preceded by a warning comment
explaining the situation and the way out. Implies --sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant