#!/usr/bin/env python3
# Python rewrite of testsuite/itemize.test.
#
# Test the output of various copy commands to ensure itemized output
# (-i, -ii) and double-verbose output (-vv) match the canonical
# representations across whole-file and delta paths.

import os
import shutil

from rsyncfns import (
    CHKFILE, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
    all_plus, allspace, dots,
    checkdiff, cp_p, makepath, run_rsync, v_filt, hardlink_symlinks_supported,
)


to2dir = TMPDIR / 'to2'

makepath(FROMDIR / 'foo', FROMDIR / 'bar' / 'baz')
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config1')
cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')
cp_p(SRCDIR / 'rsync.h', FROMDIR / 'bar' / 'baz' / 'rsync')
os.chmod(FROMDIR / 'foo' / 'config1', 0o600)
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)
os.chmod(FROMDIR / 'bar' / 'baz' / 'rsync', 0o600)

old_umask = os.umask(0)
try:
    os.symlink('../bar/baz/rsync', FROMDIR / 'foo' / 'sym')
finally:
    os.umask(old_umask)

os.link(FROMDIR / 'foo' / 'config1', FROMDIR / 'foo' / 'extra')
if to2dir.is_file():
    to2dir.unlink()


# Detect what this rsync build supports.
vv = run_rsync('-VV', check=True, capture_output=True).stdout
hardlink_symlinks = '"hardlink_symlinks": true' in vv
symtimes_supported = '"symtimes": true' in vv
# The only itemisation difference between a platform that can hard-link a
# symlink and one that cannot is the change-type letter where the symlink
# itself is transferred: hL when it is hard-linked, cL when it is copied.
# The attribute field, the "is uptodate" wording and the --copy-dest lines
# are identical either way -- measured on macOS 10.13, the one platform that
# lacks the capability (linkat(AT_FDCWD, sym, ..., 0) is EOPNOTSUPP there).
#
# The two answers can disagree, so both are asked.  The build capability is
# decided by configure on whatever filesystem the source tree sat on, while the
# link happens on whichever filesystem holds the test data: macOS builds on APFS
# (yes) and can write to HFS+ (ENOTSUP).  rsync falls back to a copy when the
# filesystem refuses, so the letter follows the filesystem.
L = ('hL' if hardlink_symlinks and hardlink_symlinks_supported(SCRATCHDIR)
     else 'cL')

if 'protocol=2' in RSYNC:
    T = '.T'
elif symtimes_supported:
    T = '.t'
else:
    T = '.T'


# First check: -iplr basic itemize on a fresh transfer.
checkdiff(['-iplr', f'{FROMDIR}/', f'{TODIR}/'],
          f"created directory {TODIR}\n"
          f"cd{all_plus} ./\n"
          f"cd{all_plus} bar/\n"
          f"cd{all_plus} bar/baz/\n"
          f">f{all_plus} bar/baz/rsync\n"
          f"cd{all_plus} foo/\n"
          f">f{all_plus} foo/config1\n"
          f">f{all_plus} foo/config2\n"
          f">f{all_plus} foo/extra\n"
          f"cL{all_plus} foo/sym -> ../bar/baz/rsync\n")

# Touch dir times so subsequent itemize diffs don't pick up dir-time noise.
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))

# Permute one file's content + mode; expect a content/mode itemize.
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')
os.chmod(FROMDIR / 'foo' / 'config2', 0o601)

checkdiff(['-iplrH', f'{FROMDIR}/', f'{TODIR}/'],
          f">f..T.{dots} bar/baz/rsync\n"
          f">f..T.{dots} foo/config1\n"
          f">f.sTp{dots} foo/config2\n"
          f"hf..T.{dots} foo/extra => foo/config1\n")

# Re-touch dirs, permute config2 again and replace the symlink target.
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))
cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')
import time
time.sleep(1)  # to provoke a directory mtime change below
(TODIR / 'foo' / 'sym').unlink()
old_umask = os.umask(0)
try:
    os.symlink('../bar/baz', TODIR / 'foo' / 'sym')
finally:
    os.umask(old_umask)
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)
os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o777)

checkdiff(['-iplrtc', f'{FROMDIR}/', f'{TODIR}/'],
          f".f..tp{dots} bar/baz/rsync\n"
          f".d..t.{dots} foo/\n"
          f".f..t.{dots} foo/config1\n"
          f">fcstp{dots} foo/config2\n"
          f"cLc{T}.{dots} foo/sym -> ../bar/baz/rsync\n")

# Re-permute config2, leaving the others untouched; lack of -t is for
# the unchanged-hard-link stress test.
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)

checkdiff(['-vvplrH', f'{FROMDIR}/', f'{TODIR}/'],
          "bar/baz/rsync is uptodate\n"
          "foo/config1 is uptodate\n"
          "foo/extra is uptodate\n"
          "foo/sym is uptodate\n"
          "foo/config2\n",
          filter=v_filt)

# Touch a mode change on one dest file then run -ii to see "no change".
os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o747)
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))

checkdiff(['-ivvplrtH', f'{FROMDIR}/', f'{TODIR}/'],
          f".d{allspace} ./\n"
          f".d{allspace} bar/\n"
          f".d{allspace} bar/baz/\n"
          f".f...p{dots} bar/baz/rsync\n"
          f".d{allspace} foo/\n"
          f".f{allspace} foo/config1\n"
          f">f..t.{dots} foo/config2\n"
          f"hf{allspace} foo/extra\n"
          f".L{allspace} foo/sym -> ../bar/baz/rsync\n",
          filter=v_filt)

# Permute one perm and re-touch a file; expect just those two itemizes.
os.chmod(TODIR / 'foo' / 'config1', 0o757)
(TODIR / 'foo' / 'config2').touch()
checkdiff(['-vplrtH', f'{FROMDIR}/', f'{TODIR}/'],
          "foo/config2\n",
          filter=v_filt)

os.chmod(TODIR / 'foo' / 'config1', 0o757)
(TODIR / 'foo' / 'config2').touch()
checkdiff(['-iplrtH', f'{FROMDIR}/', f'{TODIR}/'],
          f".f...p{dots} foo/config1\n"
          f">f..t.{dots} foo/config2\n")


# --copy-dest variants.
checkdiff(['-ivvplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
          f"cd{allspace} ./\n"
          f"cd{allspace} bar/\n"
          f"cd{allspace} bar/baz/\n"
          f"cf{allspace} bar/baz/rsync\n"
          f"cd{allspace} foo/\n"
          f"cf{allspace} foo/config1\n"
          f"cf{allspace} foo/config2\n"
          f"hf{allspace} foo/extra => foo/config1\n"
          f"cL{allspace} foo/sym -> ../bar/baz/rsync\n",
          filter=v_filt)

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-iplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
          f"created directory {to2dir}\n"
          f"hf{allspace} foo/extra => foo/config1\n")

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-vvplrtH', f'--copy-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          "./ is uptodate\n"
          "bar/ is uptodate\n"
          "bar/baz/ is uptodate\n"
          "bar/baz/rsync is uptodate\n"
          "foo/ is uptodate\n"
          "foo/config1 is uptodate\n"
          "foo/config2 is uptodate\n"
          f"foo/sym is uptodate\n"
          "foo/extra => foo/config1\n",
          filter=v_filt)


# --link-dest variants.
shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-ivvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          f"cd{allspace} ./\n"
          f"cd{allspace} bar/\n"
          f"cd{allspace} bar/baz/\n"
          f"hf{allspace} bar/baz/rsync\n"
          f"cd{allspace} foo/\n"
          f"hf{allspace} foo/config1\n"
          f"hf{allspace} foo/config2\n"
          f"hf{allspace} foo/extra => foo/config1\n"
          f"{L}{allspace} foo/sym -> ../bar/baz/rsync\n",
          filter=v_filt)

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-iplrtH', '--dry-run', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
          f"created directory {to2dir}\n")

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-iplrtH', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
          f"created directory {to2dir}\n")

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-vvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          "./ is uptodate\n"
          "bar/ is uptodate\n"
          "bar/baz/ is uptodate\n"
          "bar/baz/rsync is uptodate\n"
          "foo/ is uptodate\n"
          "foo/config1 is uptodate\n"
          "foo/config2 is uptodate\n"
          "foo/extra is uptodate\n"
          f"foo/sym is uptodate\n",
          filter=v_filt)


# --compare-dest variants.
shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-ivvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          f"cd{allspace} ./\n"
          f"cd{allspace} bar/\n"
          f"cd{allspace} bar/baz/\n"
          f".f{allspace} bar/baz/rsync\n"
          f"cd{allspace} foo/\n"
          f".f{allspace} foo/config1\n"
          f".f{allspace} foo/config2\n"
          f".f{allspace} foo/extra\n"
          f".L{allspace} foo/sym -> ../bar/baz/rsync\n",
          filter=v_filt)

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-iplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          f"created directory {to2dir}\n")

shutil.rmtree(to2dir, ignore_errors=True)
checkdiff(['-vvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
          "./ is uptodate\n"
          "bar/ is uptodate\n"
          "bar/baz/ is uptodate\n"
          "bar/baz/rsync is uptodate\n"
          "foo/ is uptodate\n"
          "foo/config1 is uptodate\n"
          "foo/config2 is uptodate\n"
          "foo/extra is uptodate\n"
          f"foo/sym is uptodate\n",
          filter=v_filt)
