Commit Graph
16 Commits
Author SHA1 Message Date
e3c8acf954 Fix test that leaks a chdir'd cwd across the test run (#17)
test_relative_symlink_to_common_file changed the process working
directory via os.chdir() but never restored it, leaving subsequent
tests running from a tmp_path fixture directory that gets deleted
after the test. Restore cwd in a finally block, matching the pattern
already used in test_pseudo_direntry.py.

Also checks off the now-satisfied xfail-marker item: no xfail markers
remain in the repo.

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 02:17:40 +00:00
b4db7c806b Prevent stale, truncated, or changing-file copies (#10)
* Prevent stale, truncated, or changing-file copies

- copy_file() now loops until every byte is actually written (os.write
  may write fewer bytes than given) and compares fstat() on the
  source's own open descriptor before/after the read loop - this
  reflects the underlying inode directly, catching a source file
  truncated, grown, or overwritten in place while being read, and isn't
  fooled by the source path being renamed away mid-copy. Raises
  BackupCreationError (already a fatal, staging-discarding condition in
  initiate_backup) on a mismatch.
- copy_file() returns a sha256 hex digest of the copied content;
  copy_direntry()/update_direntry() propagate it; rsync() carries it
  through the previously-unused msg field for CREATE/REWRITE actions.
- initiate_backup() collects those digests into a checksums dict keyed
  by path relative to the snapshot root, recorded in the completion
  manifest (already JSON since the topology-validation work).
- Added verify_snapshot(): re-hashes every file with a recorded
  checksum and reports any mismatch, satisfying the release gate's
  "completed snapshot passes manifest verification" requirement.
- Switched every mtime comparison and os.utime() restore (rsync's
  file/directory sync, hardlink_dir's directory metadata, the
  hardlink-breaking copy-on-write step) from float seconds to
  nanoseconds, so a same-second modification can't be missed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address Copilot review: hash checksums for external rsync too, fix log/error text

- initiate_backup() now hashes CREATE/REWRITE files itself when msg is
  empty (fs.rsync_ext(), the external-rsync path, never yields a
  digest). Without this, verify_snapshot() silently became a no-op for
  every snapshot created with external_rsync=True.
- Fixed a pre-existing log message that said "different inodes" for
  the branch whose actual condition is src/dst sharing an inode.
- verify_snapshot()'s error message now also mentions a corrupt
  manifest, not just missing/pre-migration, matching what
  _read_manifest() actually distinguishes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 00:10:25 +00:00
cc61aed10c Preserve symlinks and old-snapshot immutability (#9)
* Preserve symlinks and old-snapshot immutability

Two real bugs found while implementing this item, both in fs.py:

- copy_direntry() checked entry.is_dir() (default follow_symlinks=True)
  before entry.is_symlink(). A symlink pointing at a directory was
  misclassified as a directory and an empty real directory was created
  in its place, silently destroying the symlink and losing its target
  entirely. Fixed by checking is_symlink() first, and is_dir() with
  follow_symlinks=False.
- rsync()'s permission/ownership-sync branch called os.chmod/os.chown
  on dst_entry.path unconditionally. Two problems: (1) for a symlink
  whose target already matched, this had no follow_symlinks=False
  guard and could rewrite an external symlink target's permissions;
  (2) for a regular file, that path may still be hardlinked to the
  same file in the previous, completed snapshot (hardlink_dir() links
  unchanged files in at the start of every backup) - mutating it in
  place silently corrupted every older snapshot sharing the inode.
  Fixed by skipping symlinks entirely (nothing to sync once their
  target matches) and adding _break_shared_hardlink(), a copy-on-write
  step that gives a file its own inode before any in-place metadata
  change.

Also fixed the dir-mtime-restore loop's is_dir() call (same
default-follow_symlinks issue, lower severity since its own operations
were already lstat-based/no-follow).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address Copilot review: preserve ownership/timestamps when breaking hardlinks, fix test fd leak

- _break_shared_hardlink() now explicitly restores the original file's
  uid/gid and atime/mtime on the new private copy before it replaces
  the shared one. copy_file() only carries over mode at create time, so
  without this a caller that only meant to update permissions would
  silently also reset ownership/mtime as a side effect of the
  copy-on-write break itself. Also cleans up the temp file on failure
  and uses a less collision-prone temp name.
- Fixed a file descriptor leak in the new hardlink-immutability test
  (bare open(...).read() without a context manager)
- Strengthened the same test to assert mtime survives the break

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 23:08:24 +00:00
snegov 95614b6eec Add metadata preservation tests for copy_direntry
Verify that timestamps, permissions, and ownership are correctly
preserved when copying files, directories, and symlinks. Tests account
for filesystem behavior where reading a file updates its atime by
capturing source timestamps before the copy operation.
2026-02-04 21:56:25 -08:00
snegov fb9dcbd392 Refactor: split rsync tests into separate module
Move all rsync-related tests (44 tests) from test_fs.py to new
test_rsync.py module for better organization:
- TestRsync (14 tests)
- TestRsyncBasic (5 tests)
- TestParseRsyncOutput (17 tests)
- TestRsyncExt (8 tests)

Extract shared helper functions to conftest.py to eliminate duplication:
- create_file(), create_dir(), relpath()
- check_identical_file()
- common_fs_dirs fixture

This improves test organization and maintainability by grouping related
tests and removing code duplication between test modules.
2026-02-04 20:23:51 -08:00
snegov fe48010fc9 Add comprehensive tests for rsync output parsing
Implements TestParseRsyncOutput class with 17 tests covering all code
paths in _parse_rsync_output() function. Tests cover deletions, file/
directory/symlink creation, rewrites, metadata updates (permissions,
ownership, timestamps), and error handling for invalid input.
2026-02-04 19:36:24 -08:00
snegov ee1df4cb21 Update tests 2026-02-04 19:31:29 -08:00
snegov 62fcfe5881 Add running backup lock and marker of finished backup 2023-05-06 23:06:15 -07:00
snegov 2631b78d4d Handle socket files 2021-11-15 23:12:51 +03:00
snegov 84546243cb Add setup.py 2021-11-13 07:39:23 +03:00
snegov 7395268e97 Add processing of changed files 2021-11-09 00:43:18 +03:00
snegov 1ed1032f42 Add nested dir hardlink test 2021-11-02 23:45:11 +03:00
snegov f01d24f1bd Fix copying symlinks in Linux 2021-10-23 21:07:14 +03:00
snegov 862c20ed24 Add rsync support 2021-06-20 18:40:03 +03:00
snegov 17826c6978 Update hardlink tests 2021-06-16 19:42:32 +03:00
snegov 11c8197dba Add tests for fs.hardlink_dir() 2021-06-15 23:05:09 +03:00