rsync

The atago project wrote these specs on its own initiative and runs them in its own CI, to exercise atago against a real program. They are not rsync's official test suite, and the rsync project is not affiliated with atago.

Summary #

3 suites · 31 scenarios

Contents #

Filter rules are where rsync surprises people most, and a wrong rule does not fail: it silently copies too much or too little. This suite fixes the rules by the tree they produce, asserted with dir: and changes:, so a pattern that matches one path more or fewer turns a scenario red.

It covers an unanchored pattern against an anchored one; the include-directories-then-exclude-everything idiom and the empty directory chains it leaves unless --prune-empty-dirs is given; excluded files at the destination surviving --delete and going away with --delete-excluded; rule files read with --exclude-from; an explicit --files-from list; and the three ways rsync treats a symlink: kept as a link by -a, replaced by its target's bytes by -L, and skipped with only a stdout notice and exit 0 by a bare -r.

All trees are written by the spec, so nothing from rsync is committed.

Source: test/e2e/thirdparty/rsync/filters.atago.yaml

Scenario: an unanchored pattern matches at any depth, an anchored one only at the root #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.log is created.
  • Fixture file src/keep/b.log is created.
  • Fixture file src/keep/c.txt is created.
  • Fixture file src/cache/d.txt is created.
  • Fixture file src/keep/cache/e.txt is created.
  • Fixture file rules is created.

Inputs #

Fixture src/a.log:

1

Fixture src/keep/b.log:

2

Fixture src/keep/c.txt:

3

Fixture src/cache/d.txt:

4

Fixture src/keep/cache/e.txt:

5

Fixture rules:

*.log
/cache/

When #

rsync -a --exclude-from=rules src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/keep/c.txt, dst/keep/cache/e.txt, modified nothing, deleted nothing
  • file dst/cache does not exist

Scenario: including directories then excluding everything else leaves empty chains #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/top.log is created.
  • Fixture file src/a/b/deep.log is created.
  • Fixture file src/a/b/deep.txt is created.
  • Fixture file src/only_txt/x.txt is created.
  • Fixture file rules is created.

Inputs #

Fixture src/top.log:

1

Fixture src/a/b/deep.log:

2

Fixture src/a/b/deep.txt:

3

Fixture src/only_txt/x.txt:

4

Fixture rules:

+ */
+ *.log
- *

When #

rsync -a --filter=". rules" src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/top.log, dst/a/b/deep.log, modified nothing, deleted nothing
  • dir dst/only_txt has 0 entries

Scenario: --prune-empty-dirs drops the directory chains that end up empty #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/top.log is created.
  • Fixture file src/a/b/deep.log is created.
  • Fixture file src/only_txt/nested/x.txt is created.
  • Fixture file rules is created.

Inputs #

Fixture src/top.log:

1

Fixture src/a/b/deep.log:

2

Fixture src/only_txt/nested/x.txt:

4

Fixture rules:

+ */
+ *.log
- *

When #

rsync -a -m --filter=". rules" src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/top.log, dst/a/b/deep.log, modified nothing, deleted nothing
  • dir dst contains top.log, contains a, has 2 entries
  • file dst/only_txt does not exist

Scenario: --delete spares excluded destination files; --delete-excluded removes them #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/app.conf is created.
  • Fixture file dst/app.conf is created.
  • Fixture file dst/local.secret is created.
  • Fixture file dst/stray.txt is created.

Inputs #

Fixture src/app.conf:

conf

Fixture dst/app.conf:

conf

Fixture dst/local.secret:

do not lose me

Fixture dst/stray.txt:

stray

When #

rsync -a --delete --exclude=*.secret src/ dst/
rsync -a --delete --delete-excluded --exclude=*.secret src/ dst/

Then #

  • after rsync -a --delete --exclude=*.secret src/ dst/:
    • exit code is 0
    • the step changed exactly created nothing, modified nothing, deleted dst/stray.txt
    • file dst/local.secret contains do not lose me
  • after rsync -a --delete --delete-excluded --exclude=*.secret src/ dst/:
    • exit code is 0
    • the step changed exactly created nothing, modified nothing, deleted dst/local.secret

Scenario: --files-from copies exactly the listed paths and keeps their layout #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file src/b.txt is created.
  • Fixture file src/sub/c.txt is created.
  • Fixture file src/sub/d.txt is created.
  • Fixture file list is created.

Inputs #

Fixture src/a.txt:

a

Fixture src/b.txt:

b

Fixture src/sub/c.txt:

c

Fixture src/sub/d.txt:

d

Fixture list:

a.txt
sub/c.txt

When #

rsync -a --files-from=list src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/a.txt, dst/sub/c.txt, modified nothing, deleted nothing

Scenario: --files-from: a listed path that does not exist is exit 23 #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file list is created.

Inputs #

Fixture src/a.txt:

a

Fixture list:

a.txt
ghost.txt

When #

rsync -a --files-from=list src/ dst/

Then #

  • exit code is 23
  • stdout is empty
  • stderr contains ghost.txt" failed: No such file or directory, (code 23)
  • the step changed exactly created dst/a.txt, modified nothing, deleted nothing

only when rsync --mkpath --version succeeds · skipped on Windows

Given #

  • Fixture file src/target.txt is created.

Inputs #

Fixture src/target.txt:

target bytes

When #

ln -s target.txt src/link
rsync -a src/ dst/
readlink dst/link

Then #

  • after rsync -a src/ dst/:
    • exit code is 0
  • after readlink dst/link:
    • exit code is 0
    • stdout equals an exact value

Expected output #

expected stdout:

target.txt

only when rsync --mkpath --version succeeds · skipped on Windows

Given #

  • Fixture file src/target.txt is created.

Inputs #

Fixture src/target.txt:

target bytes

When #

ln -s target.txt src/link
rsync -rL src/ dst/
test -L dst/link

Then #

  • after rsync -rL src/ dst/:
    • exit code is 0
    • file dst/link is byte-identical to src/target.txt
  • after test -L dst/link:
    • exit code is 1

only when rsync --mkpath --version succeeds · skipped on Windows

Given #

  • Fixture file src/target.txt is created.

Inputs #

Fixture src/target.txt:

target bytes

When #

ln -s target.txt src/link
rsync -r src/ dst/

Then #

  • after rsync -r src/ dst/:
    • exit code is 0
    • stdout equals an exact value
    • stderr is empty
    • the step changed exactly created dst/target.txt, modified nothing, deleted nothing
    • file dst/link does not exist

Expected output #

expected stdout:

skipping non-regular file "link"

rsync (exit codes and stream separation) #

rsync documents a table of exit codes, and a backup script branches on them: 0 means the destination now mirrors the source, 23 means some files did not make it, 25 means a safety limit stopped deletions part way. This suite pins every code that a local copy can reach — 0, 1 for a usage error, 11 for a file I/O error, 23 for a partial transfer, and 25 for the --max-delete limit — together with what each one leaves behind on disk, because a code is only useful when it tells the truth about the tree.

Every failure is also checked for stream separation: the diagnosis goes to stderr and stdout stays empty, so a script that parses stdout never mistakes an error for a transfer log. The rsync error: ... at main.c(N) trailer carries a source line number that moves between releases, so the specs match the stable prefix only.

All trees are written by the spec, so nothing from rsync is committed.

Source: test/e2e/thirdparty/rsync/rsync.atago.yaml

Scenario: a successful copy is silent and exits 0 #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file src/sub/b.txt is created.
  • The command runs with an isolated home under ${workdir}/.atago-home (HOME/XDG or APPDATA redirected).

Inputs #

Fixture src/a.txt:

alpha

Fixture src/sub/b.txt:

bravo

When #

rsync -a src/ dst/

Then #

  • exit code is 0
  • stdout is empty
  • stderr is empty
  • the step changed exactly created dst/a.txt, dst/sub/b.txt, modified nothing, deleted nothing

Scenario: exit 1: an unknown option is a usage error that copies nothing #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.

Inputs #

Fixture src/a.txt:

alpha

When #

rsync -a --no-such-option src/ dst/

Then #

  • exit code is 1
  • stdout is empty
  • stderr contains --no-such-option: unknown option, rsync error: syntax or usage error (code 1)
  • the step changed exactly created nothing, modified nothing, deleted nothing

Scenario: exit 1: no arguments prints the usage on stderr, not stdout #

only when rsync --mkpath --version succeeds

When #

rsync

Then #

  • exit code is 1
  • stdout is empty
  • stderr contains Usage: rsync [OPTION]... SRC [SRC]... DEST, rsync error: syntax or usage error (code 1)

Scenario: a forgotten destination turns a --delete sync into a listing that exits 0 #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.

Inputs #

Fixture src/a.txt:

alpha

When #

rsync -a --delete src/

Then #

  • exit code is 0
  • stdout matches /(?m) a\.txt$/
  • stderr is empty
  • the step changed exactly created nothing, modified nothing, deleted nothing

Scenario: exit 11: a destination whose parent is missing fails and creates nothing #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.

Inputs #

Fixture src/a.txt:

alpha

When #

rsync -a src/ missing/parent/dst/

Then #

  • exit code is 11
  • stdout is empty
  • stderr contains mkdir ", missing/parent/dst" failed: No such file or directory, rsync error: error in file IO (code 11)
  • file missing does not exist

Scenario: --mkpath creates the missing parents that exit 11 refused #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.

Inputs #

Fixture src/a.txt:

alpha

When #

rsync -a --mkpath src/ missing/parent/dst/

Then #

  • exit code is 0
  • stdout is empty
  • the step changed exactly created missing/parent/dst/a.txt, modified nothing, deleted nothing
  • file missing/parent/dst/a.txt is byte-identical to src/a.txt

Scenario: exit 23: a missing source is reported as a partial transfer #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file present.txt is created.

Inputs #

Fixture present.txt:

here

When #

rsync -a present.txt absent.txt dst/

Then #

  • exit code is 23
  • stdout is empty
  • stderr contains link_stat ", absent.txt" failed: No such file or directory, some files/attrs were not transferred, (code 23)
  • the step changed exactly created dst/present.txt, modified nothing, deleted nothing

Scenario: exit 23: an unreadable file is skipped and everything else still arrives #

only when rsync --mkpath --version succeeds · skipped when test "$(id -u)" = 0 succeeds

Given #

  • Fixture file src/ok.txt is created.
  • Fixture file src/locked.txt is created.

Inputs #

Fixture src/ok.txt:

readable

Fixture src/locked.txt:

secret

When #

rsync -a src/ dst/

Then #

  • exit code is 23
  • stdout is empty
  • stderr contains locked.txt": Permission denied, (code 23)
  • the step changed exactly created dst/ok.txt, modified nothing, deleted nothing
  • file dst/locked.txt does not exist

Scenario: exit 25: --max-delete stops deleting at the limit and says how many it kept #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file dst/a.txt is created.
  • Fixture file dst/extra1 is created.
  • Fixture file dst/extra2 is created.
  • Fixture file dst/extra3 is created.

Inputs #

Fixture src/a.txt:

alpha

Fixture dst/a.txt:

alpha

Fixture dst/extra1:

x

Fixture dst/extra2:

x

Fixture dst/extra3:

x

When #

rsync -a --delete --max-delete=1 src/ dst/

Then #

  • exit code is 25
  • stdout is empty
  • stderr contains Deletions stopped due to --max-delete limit (2 skipped), (code 25)
  • dir dst contains a.txt, has 3 entries

Scenario: listing a single source prints it and changes nothing #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file src/sub/b.txt is created.

Inputs #

Fixture src/a.txt:

alpha

Fixture src/sub/b.txt:

bravo

When #

rsync src/

Then #

  • exit code is 0
  • stdout matches /(?m)^d[rwx-]{9} +[0-9,]+ [0-9/]+ [0-9:]+ \.$/
  • stderr is empty
  • the step changed exactly created nothing, modified nothing, deleted nothing
  • stdout matches /(?m)^-[rwx-]{9} +6 [0-9/]+ [0-9:]+ a\.txt$/
  • stdout matches /(?m)^d[rwx-]{9} +[0-9,]+ [0-9/]+ [0-9:]+ sub$/
  • stdout does not contain b.txt

rsync (what a sync changes and what it leaves alone) #

An rsync run is judged by the tree it leaves behind. This suite asserts that tree with changes:, which is exhaustive in both directions, so a scenario fails when rsync writes, rewrites, or deletes one path more or one path fewer than the contract says.

The contracts covered are the ones a backup or deploy script leans on: the trailing slash on the source decides whether the directory itself or only its contents is copied; a second identical run is a no-op; --dry-run reports the plan and changes nothing, and the real run then does exactly that plan; --delete removes only what the source no longer has; the quick check trusts size and modification time, so a same-size, same-time file with different bytes is skipped unless --checksum is given; --update, --ignore-existing, and --backup each protect a destination file in their own way; --remove-source-files moves files but never directories; and the bytes that arrive are the bytes that left, checked with equals_file for an empty file, NUL bytes, CRLF without a final newline, and a file name with spaces and non-ASCII characters.

All trees are written by the spec, so nothing from rsync is committed.

Source: test/e2e/thirdparty/rsync/sync.atago.yaml

Scenario: a trailing slash copies the contents, no slash copies the directory #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file src/sub/b.txt is created.

Inputs #

Fixture src/a.txt:

alpha

Fixture src/sub/b.txt:

bravo

When #

rsync -a src dir_itself
rsync -a src/ contents_only

Then #

  • after rsync -a src dir_itself:
    • exit code is 0
    • the step changed exactly created dir_itself/src/a.txt, dir_itself/src/sub/b.txt, modified nothing, deleted nothing
  • after rsync -a src/ contents_only:
    • exit code is 0
    • the step changed exactly created contents_only/a.txt, contents_only/sub/b.txt, modified nothing, deleted nothing

Scenario: a second identical run is a no-op #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/a.txt is created.
  • Fixture file src/sub/b.txt is created.

Inputs #

Fixture src/a.txt:

alpha

Fixture src/sub/b.txt:

bravo

When #

rsync -a src/ dst/
rsync -a -i src/ dst/

Then #

  • after rsync -a src/ dst/:
    • exit code is 0
  • after rsync -a -i src/ dst/:
    • exit code is 0
    • stdout is empty
    • the step changed exactly created nothing, modified nothing, deleted nothing

Scenario: --dry-run reports the plan and changes nothing, then the real run does exactly that #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/new.txt is created.
  • Fixture file src/same.txt is created.
  • Fixture file dst/same.txt is created.
  • Fixture file dst/stale.txt is created.

Inputs #

Fixture src/new.txt:

new

Fixture src/same.txt:

same

Fixture dst/same.txt:

same

Fixture dst/stale.txt:

stale

When #

touch -r src/same.txt dst/same.txt
rsync -a -i --delete --dry-run src/ dst/
rsync -a -i --delete src/ dst/

Then #

  • after rsync -a -i --delete --dry-run src/ dst/:
    • exit code is 0
    • stdout contains *deleting stale.txt, >f+++++++++ new.txt
    • the step changed exactly created nothing, modified nothing, deleted nothing
    • stdout does not contain same.txt
  • after rsync -a -i --delete src/ dst/:
    • exit code is 0
    • stdout contains *deleting stale.txt, >f+++++++++ new.txt
    • the step changed exactly created dst/new.txt, modified nothing, deleted dst/stale.txt

Scenario: --delete removes extraneous files and directories and nothing else #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/keep.txt is created.
  • Fixture file src/sub/keep.txt is created.
  • Fixture file dst/keep.txt is created.
  • Fixture file dst/sub/keep.txt is created.
  • Fixture file dst/sub/gone.txt is created.
  • Fixture file dst/olddir/deep/gone.txt is created.

Inputs #

Fixture src/keep.txt:

keep

Fixture src/sub/keep.txt:

keep

Fixture dst/keep.txt:

keep

Fixture dst/sub/keep.txt:

keep

Fixture dst/sub/gone.txt:

gone

Fixture dst/olddir/deep/gone.txt:

gone

When #

rsync -a --delete src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created nothing, deleted dst/sub/gone.txt, dst/olddir/deep/gone.txt
  • file dst/olddir does not exist

Scenario: without --delete an extraneous destination file survives #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/keep.txt is created.
  • Fixture file dst/extra.txt is created.

Inputs #

Fixture src/keep.txt:

keep

Fixture dst/extra.txt:

mine

When #

rsync -a src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/keep.txt, modified nothing, deleted nothing

Scenario: the quick check skips a same-size, same-mtime file that --checksum then fixes #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/f.txt is created.
  • Fixture file dst/f.txt is created.

Inputs #

Fixture src/f.txt:

AAAA

Fixture dst/f.txt:

BBBB

When #

touch -r src/f.txt dst/f.txt
rsync -a -i src/ dst/
rsync -a -i -c src/ dst/

Then #

  • after rsync -a -i src/ dst/:
    • exit code is 0
    • stdout does not contain f.txt
    • the step changed exactly created nothing, modified nothing, deleted nothing
  • after rsync -a -i -c src/ dst/:
    • exit code is 0
    • stdout contains >fc........ f.txt
    • the step changed exactly created nothing, modified dst/f.txt, deleted nothing
    • file dst/f.txt is byte-identical to src/f.txt

Scenario: -a preserves the mtime, so the next run skips; -r alone does not #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/f.txt is created.

Inputs #

Fixture src/f.txt:

payload

When #

touch -t 202001020304.05 src/f.txt
rsync -r src/ plain/
rsync -r -i src/ plain/
rsync -a src/ archived/
rsync -a -i src/ archived/
find archived/f.txt plain/f.txt -newer src/f.txt

Then #

  • after rsync -r -i src/ plain/:
    • exit code is 0
    • stdout equals an exact value
  • after rsync -a -i src/ archived/:
    • exit code is 0
    • stdout is empty
  • after find archived/f.txt plain/f.txt -newer src/f.txt:
    • stdout equals an exact value

Expected output #

expected stdout:

>f..T...... f.txt

expected stdout:

plain/f.txt

Scenario: --update keeps a destination file that is newer than the source #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/f.txt is created.
  • Fixture file dst/f.txt is created.

Inputs #

Fixture src/f.txt:

old source

Fixture dst/f.txt:

newer edit

When #

touch -t 202001020304.05 src/f.txt
rsync -a -u src/ dst/
rsync -a src/ dst/

Then #

  • after rsync -a -u src/ dst/:
    • exit code is 0
    • the step changed exactly created nothing, modified nothing, deleted nothing
    • file dst/f.txt contains newer edit
  • after rsync -a src/ dst/:
    • exit code is 0
    • the step changed exactly created nothing, modified dst/f.txt, deleted nothing

Scenario: --ignore-existing adds new files and never touches existing ones #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/exists.txt is created.
  • Fixture file src/fresh.txt is created.
  • Fixture file dst/exists.txt is created.

Inputs #

Fixture src/exists.txt:

from source

Fixture src/fresh.txt:

fresh

Fixture dst/exists.txt:

hand edited

When #

rsync -a --ignore-existing src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/fresh.txt, modified nothing, deleted nothing
  • file dst/exists.txt contains hand edited

Scenario: --backup keeps the replaced destination file under the suffix #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/f.txt is created.
  • Fixture file dst/f.txt is created.

Inputs #

Fixture src/f.txt:

version two, longer

Fixture dst/f.txt:

version one

When #

rsync -a --backup --suffix=.bak src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/f.txt.bak, modified dst/f.txt, deleted nothing
  • file dst/f.txt.bak contains version one
  • file dst/f.txt is byte-identical to src/f.txt

Scenario: --remove-source-files moves the files and leaves the directories #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/one.txt is created.
  • Fixture file src/sub/two.txt is created.

Inputs #

Fixture src/one.txt:

1

Fixture src/sub/two.txt:

2

When #

rsync -a --remove-source-files src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/one.txt, dst/sub/two.txt, modified nothing, deleted src/one.txt, src/sub/two.txt
  • dir src contains sub, has 1 entry

Scenario: awkward bytes and file names arrive unchanged #

only when rsync --mkpath --version succeeds

Given #

  • Fixture file src/empty.bin is created.
  • Fixture file src/nul.bin is created.
  • Fixture file src/mixed.txt is created.
  • Fixture file src/with space/日本語 ファイル.txt is created.

Inputs #

Fixture src/mixed.txt:

unix
windows
no trailing newline

Fixture src/with space/日本語 ファイル.txt:

名前

When #

rsync -a src/ dst/

Then #

  • exit code is 0
  • the step changed exactly created dst/empty.bin, dst/nul.bin, dst/mixed.txt, dst/with space/日本語 ファイル.txt, modified nothing, deleted nothing
  • file dst/empty.bin is byte-identical to src/empty.bin
  • file dst/nul.bin is byte-identical to src/nul.bin
  • file dst/mixed.txt is byte-identical to src/mixed.txt
  • file dst/with space/日本語 ファイル.txt is byte-identical to src/with space/日本語 ファイル.txt