# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Lint as: python3
"""cdc_rsync upload test."""

import json
import logging
import os
import subprocess
import time

from integration_tests.framework import utils
from integration_tests.cdc_rsync import test_base


class UploadTest(test_base.CdcRsyncTest):
  """cdc_rsync upload test class."""

  def test_single_uncompressed(self):
    """Uploads and syncs a file uncompressed."""

    self._do_test_single(compressed=False)

  def test_upload_compressed(self):
    """Uploads and syncs a file compressed."""

    self._do_test_single(compressed=True)

  def _do_test_single(self, compressed):
    """Runs rsync 3 times and validates results.

       1) Uploads a file, checks sha1 hashes.
       2) Uploads the same file again, checks nothing changed.
       3) Modifies the file and uploads again. Checks sha1 hashes.

    Args:
        compressed (bool): Whether to append '--compress' or not.
    """
    compressed_arg = '--compress' if compressed else None

    utils.create_test_file(self.local_data_path, 1024)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=1))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))

    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, matching=1))

    utils.create_test_file(self.local_data_path, 2534)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))

  def test_backslash_in_dest_folder(self):
    r"""Verifies uploading to \mnt\developer."""

    filepath = os.path.join(self.local_base_dir, 'file1.txt')
    utils.create_test_file(filepath, 1)
    res = utils.run_rsync(filepath, self.remote_base_dir.replace('/', '\\'))
    self.assertTrue(utils.files_count_is(res, missing=1))
    self._assert_remote_dir_contains(['file1.txt'])

  def test_backslash_in_source_folder(self):
    r"""Verifies uploading from /source/folder."""

    filepath = os.path.join(self.local_base_dir, 'file1.txt')
    utils.create_test_file(filepath, 1)
    filepath = filepath.replace('\\', '/')
    res = utils.run_rsync(filepath, self.remote_base_dir)
    self.assertTrue(utils.files_count_is(res, missing=1))
    self._assert_remote_dir_contains(['file1.txt'])

  def test_single_unicode(self):
    """Uploads a file with a non-ascii unicode path and checks sha1 signatures."""

    nonascii_local_data_path = self.local_base_dir + '⛽⛽⛽⛽⛽⛽⛽⛽.dat'
    nonascii_remote_data_path = self.remote_base_dir + '⛽⛽⛽⛽⛽⛽⛽⛽.dat'
    utils.create_test_file(nonascii_local_data_path, 1024)
    # In order to check that non-ascii characters are not considered as
    # wildcard
    # ?  characters, create a second file.  Only 1 file should be uploaded.
    utils.create_test_file(self.local_data_path, 1024)
    res = utils.run_rsync(nonascii_local_data_path, self.remote_base_dir, None)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=1))
    self.assertTrue(
        utils.sha1_matches(nonascii_local_data_path, nonascii_remote_data_path))

  def test_uncompressed_no_empty_folders(self):
    """Uploads and syncs multiple files uncompressed in different folders."""

    self._do_test_no_empty_folders(compressed=False)

  def test_compressed_no_empty_folders(self):
    """Uploads and syncs multiple files compressed in different folders."""

    self._do_test_no_empty_folders(compressed=True)

  def _do_test_no_empty_folders(self, compressed):
    """Runs rsync with(out) -r for a non-trivial directory and validates results.

       1) Uploads a source directory with -r, checks sha1 hashes.
       |-- rootdir
       |   |-- dir1
       |      |-- file1_1.txt
       |      |-- file1_2.txt
       |   |-- dir2
       |      |-- file2_1.txt
       |   |-- file0.txt
       2) Uploads the same source directory again without -r,
       checks nothing has changed. The directory should be just skipped.
       3) Uploads the same source directory with --delete option and with -r.
       Nothing should change.
       4) Removes dir1 and dir2 locally.
       Uploads the same source directory with --delete option and with -r.
       dir1 and dir2 should be removed from the remote instance.

    Args:
        compressed (bool): Whether to append '--compress' or not.
    """
    compressed_arg = '--compress' if compressed else None
    local_root_path = self.local_base_dir + 'rootdir'
    remote_root_path = self.remote_base_dir + 'rootdir/'
    utils.create_test_file(local_root_path + '\\dir1\\file1_1.txt', 1024)
    utils.create_test_file(local_root_path + '\\dir1\\file1_2.txt', 1024)
    utils.create_test_file(local_root_path + '\\dir2\\file2_1.txt', 1024)
    utils.create_test_file(local_root_path + '\\file0.txt', 1024)
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=4, missing_dir=3))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir1\\file1_1.txt',
                           remote_root_path + 'dir1/file1_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir1\\file1_2.txt',
                           remote_root_path + 'dir1/file1_2.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir2\\file2_1.txt',
                           remote_root_path + 'dir2/file2_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\file0.txt',
                           remote_root_path + 'file0.txt'))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, extraneous_dir=1))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, matching=4, matching_dir=3))

    utils.remove_test_directory(local_root_path + '\\dir1\\')
    utils.remove_test_directory(local_root_path + '\\dir2\\')
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(
            res, matching=1, extraneous=3, matching_dir=1, extraneous_dir=2))
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'dir1'))
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'dir2'))

  def _do_test_no_empty_folders_with_backslash(self, compressed):
    """Runs rsync with(out) -r for a non-trivial directory with a trailing backslash.

       1) Uploads a source directory with -r, checks sha1 hashes.
       Everything from rootdir should be copied except rootdir itself.
       |-- rootdir
       |   |-- dir1
       |      |-- file1_1.txt
       |      |-- file1_2.txt
       |   |-- dir2
       |      |-- file2_1.txt
       |   |-- file0.txt
       2) Uploads the same source directory again without -r,
       checks nothing has changed. The directory should be just skipped.
       3) Uploads the same source directory with --delete option and with -r.
       Nothing should change.
       4) Removes dir1 and dir2 locally.
       Uploads the same source directory with --delete option and with -r.
       dir1 and dir2 should be removed from the remote instance.

    Args:
        compressed (bool): Whether to append '--compress' or not.
    """
    compressed_arg = '--compress' if compressed else None
    local_root_path = self.local_base_dir + 'rootdir\\'
    utils.create_test_file(local_root_path + 'dir1\\file1_1.txt', 1024)
    utils.create_test_file(local_root_path + 'dir1\\file1_2.txt', 1024)
    utils.create_test_file(local_root_path + 'dir2\\file2_1.txt', 1024)
    utils.create_test_file(local_root_path + 'file0.txt', 1024)
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=4, missing_dir=2))
    self.assertTrue(
        utils.sha1_matches(local_root_path + 'dir1\\file1_1.txt',
                           self.remote_base_dir + 'dir1/file1_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + 'dir1\\file1_2.txt',
                           self.remote_base_dir + 'dir1/file1_2.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + 'dir2\\file2_1.txt',
                           self.remote_base_dir + 'dir2/file2_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + 'file0.txt',
                           self.remote_base_dir + 'file0.txt'))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(
        res, extraneous=1, extraneous_dir=2))  # file0.txt, dir1, dir2

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, matching=4, matching_dir=2))

    utils.remove_test_directory(local_root_path + '\\dir1\\')
    utils.remove_test_directory(local_root_path + '\\dir2\\')
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(res, matching=1, extraneous=3, extraneous_dir=2))
    self.assertFalse(
        utils.does_directory_exist_remotely(self.remote_base_dir + 'dir1'))
    self.assertFalse(
        utils.does_directory_exist_remotely(self.remote_base_dir + 'dir2'))

  def test_uncompressed_no_empty_folders_with_backslash(self):
    """Uploads multiple files uncompressed from a folder with a trailing backslash."""

    self._do_test_no_empty_folders_with_backslash(compressed=False)

  def test_compressed_no_empty_folders_with_backslash(self):
    """Uploads multiple files compressed from a folder with a trailing backslash."""

    self._do_test_no_empty_folders_with_backslash(compressed=True)

  def test_uncompressed_with_empty_folders(self):
    """Uploads and syncs multiple files uncompressed and empty folders."""

    self._do_test_with_empty_folders(compressed=False)

  def test_compressed_with_empty_folders(self):
    """Uploads and syncs multiple files compress and empty folders."""

    self._do_test_with_empty_folders(compressed=True)

  def _do_test_with_empty_folders(self, compressed):
    """Runs rsync with(out) -r for a non-trivial directory with empty folders.

       1) Uploads a source directory with -r, checks sha1 hashes.
       |-- rootdir
       |   |-- dir1
       |      |-- emptydir2
       |      |-- file1_1.txt
       |      |-- file1_2.txt
       |   |-- dir2
       |      |-- file2_1.txt
       |   |-- emptydir1
       |   |-- file0.txt
       2) Uploads the same source directory again without -r,
       checks nothing has changed. The directory should be just skipped.
       3) Uploads the same source directory with --delete option and with -r.
       Nothing should change.
       4) Removes dir1 and dir2 locally.
       Uploads the same source directory with --delete option and with -r.
       dir1 and dir2 should be removed from the remote instance.

    Args:
        compressed (bool): Whether to append '--compress' or not.
    """
    compressed_arg = '--compress' if compressed else None
    local_root_path = self.local_base_dir + 'rootdir'
    remote_root_path = self.remote_base_dir + 'rootdir/'
    utils.create_test_file(local_root_path + '\\dir1\\file1_1.txt', 1024)
    utils.create_test_file(local_root_path + '\\dir1\\file1_2.txt', 1024)
    utils.create_test_directory(local_root_path + '\\dir1\\emptydir2\\')
    utils.create_test_file(local_root_path + '\\dir2\\file2_1.txt', 1024)
    utils.create_test_file(local_root_path + '\\file0.txt', 1024)
    utils.create_test_directory(local_root_path + '\\emptydir1\\')
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=4, missing_dir=5))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir1\\file1_1.txt',
                           remote_root_path + 'dir1/file1_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir1\\file1_2.txt',
                           remote_root_path + 'dir1/file1_2.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\dir2\\file2_1.txt',
                           remote_root_path + 'dir2/file2_1.txt'))
    self.assertTrue(
        utils.sha1_matches(local_root_path + '\\file0.txt',
                           remote_root_path + 'file0.txt'))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, extraneous_dir=1))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, matching=4, matching_dir=5))

    utils.remove_test_directory(local_root_path + '\\dir1\\')
    utils.remove_test_directory(local_root_path + '\\dir2\\')
    res = utils.run_rsync(local_root_path, self.remote_base_dir, compressed_arg,
                          '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(
            res, matching=1, extraneous=3, matching_dir=2, extraneous_dir=3))
    self.assertIn('3/3 file(s) and 3/3 folder(s) deleted', res.stdout)
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'dir1'))
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'dir2'))

  def test_upload_empty_file(self):
    """Uploads an empty file and checks sha1 signatures."""

    empty_local_data_path = self.local_base_dir + 'emptyfile.dat'
    empty_remote_data_path = self.remote_base_dir + 'emptyfile.dat'
    utils.create_test_file(empty_local_data_path, 0)
    res = utils.run_rsync(empty_local_data_path, self.remote_base_dir, None)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=1))
    self.assertTrue(
        utils.sha1_matches(empty_local_data_path, empty_remote_data_path))

  def test_upload_empty_folder_with_backslash(self):
    """Uploads an empty folder with a trailing backslash."""

    self._do_test_upload_empty_folder(with_backslash=True)

  def test_upload_empty_folder_no_backslash(self):
    """Uploads an empty folder without a trailing backslash."""

    self._do_test_upload_empty_folder(with_backslash=False)

  def _do_test_upload_empty_folder(self, with_backslash=False):
    """Uploads an empty folder."""

    local_data_dir = (
        self.local_base_dir +
        'empty_folder\\' if with_backslash else self.local_base_dir +
        'empty_folder')
    res = utils.run_rsync(local_data_dir, self.remote_base_dir, None)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=0))

  def test_whole_file_uncompressed(self):
    """Uploads and syncs a file uncompressed with --whole-file."""

    self._do_test_whole_file(compressed=False)

  def test_whole_file_compressed(self):
    """Uploads and syncs a file compressed with --whole-file."""

    self._do_test_whole_file(compressed=True)

  def _do_test_whole_file(self, compressed):
    """Runs rsync 3 times with --whole-file -v options and validates results.

       1) Uploads a file.
       2) Modifies the file and uploads it with --whole-file and -v options.
       Checks the output contains C100%, not D100%.
       3) Modifies the file and uploads it with -W and -v options.
       Checks the output contains C100%, not D100%.

    Args:
        compressed (bool): Whether to append '--compress' or not.
    """
    compressed_arg = '--compress' if compressed else None

    utils.create_test_file(self.local_data_path, 1024)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg)

    utils.create_test_file(self.local_data_path, 2534)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg, '--whole-file', '-v')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    self.assertIn('will be copied due to -W/--whole-file', str(res.stdout))
    self.assertIn('C100%', str(res.stdout))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))

    utils.create_test_file(self.local_data_path, 3456)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          compressed_arg, '-W', '-v')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    self.assertIn('C100%', str(res.stdout))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))

  def test_keep_file_permissions(self):
    """Verifies that file permissions are kept for changed files."""

    # Upload a file and check permissions.
    utils.create_test_file(self.local_data_path, 1024)
    utils.run_rsync(self.local_data_path, self.remote_base_dir)
    ls_res = utils.get_ssh_command_output('ls -al %s' % self.remote_data_path)
    self.assertIn('-rw-r--r--', ls_res)

    # Add executable bit.
    utils.get_ssh_command_output('chmod a+x %s*' % self.remote_data_path)
    ls_res = utils.get_ssh_command_output('ls -al %s' % self.remote_data_path)
    self.assertIn('-rwxr-xr-x', ls_res)

    # Sync file again and verify permissions don't change.
    utils.create_test_file(self.local_data_path, 1337)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    ls_res = utils.get_ssh_command_output('ls -al %s' % self.remote_data_path)
    self.assertIn('-rwxr-xr-x', ls_res)

  def test_include_exclude(self):
    """Verifies the --include and --exclude options."""

    files = [
        'file1.txt', 'folder1\\file2.txt', 'folder1\\file3.dat',
        'folder1\\folder2\\file4.txt', 'folder3\\file5.txt'
    ]

    for file in files:
      utils.create_test_file(self.local_base_dir + file, 987)

    # Upload file2.txt and file3.dat.
    res = utils.run_rsync(self.local_base_dir + '*', self.remote_base_dir, '-r',
                          '--include=*\\file2.txt', '--exclude=*.txt')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=2, missing_dir=3))
    self._assert_remote_dir_contains(['folder1/file2.txt', 'folder1/file3.dat'])

    # Upload all except *.dat with --delete, make sure file3.dat is kept.
    utils.remove_test_file(self.local_base_dir + 'folder1\\file3.dat')
    res = utils.run_rsync(self.local_base_dir + '*', self.remote_base_dir, '-r',
                          '--delete', '--exclude=*.dat')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(res, missing=3, matching=1, matching_dir=3))
    self._assert_remote_dir_contains([
        'file1.txt', 'folder1/file2.txt', 'folder1/file3.dat',
        'folder1/folder2/file4.txt', 'folder3/file5.txt'
    ])

  def test_exclude_include_from(self):
    """Verifies the --include-from and --exclude-from options."""

    files = [
        'file1.txt', 'folder1\\file2.txt', 'folder1\\file3.dat',
        'folder1\\folder2\\file4.txt', 'folder3\\file5.txt'
    ]

    for file in files:
      utils.create_test_file(self.local_base_dir + file, 987)

    include_file = self.local_base_dir + 'include.txt'
    with open(include_file, 'wt') as f:
      f.writelines(['file1.txt\n', 'folder3\\file5.txt'])

    exclude_file = self.local_base_dir + 'exclude.txt'
    with open(exclude_file, 'wt') as f:
      f.writelines(['*.txt'])

    res = utils.run_rsync('-r', '--include-from', include_file,
                          '--exclude-from', exclude_file,
                          self.local_base_dir + '*', self.remote_base_dir)
    self.assertTrue(utils.files_count_is(res, missing=3, missing_dir=3))
    self._assert_remote_dir_contains(
        ['file1.txt', 'folder1/file3.dat', 'folder3/file5.txt'])

  def test_files_from(self):
    """Verifies the --files-from option."""

    files = [
        'file1.txt', 'folder1\\file2.txt', 'folder1\\file3.dat',
        'folder1\\folder2\\file4.txt', 'folder3\\file5.txt'
    ]

    for file in files:
      utils.create_test_file(self.local_base_dir + file, 987)

    sources_file = self.local_base_dir + 'sources.txt'
    with open(sources_file, 'wt') as f:
      f.writelines([
          'file1.txt\n',
          '\n',
          '  folder1\\file3.dat  \n',
          'folder1\\.\\folder2\\file4.txt\n',  # .\\ = rel path marker
          '  folder3\\file5.txt\n',
          '\n'
      ])

    res = utils.run_rsync('--files-from', sources_file, self.local_base_dir,
                          self.remote_base_dir)
    self.assertTrue(utils.files_count_is(res, missing=4))
    self._assert_remote_dir_contains([
        'file1.txt', 'folder1/file3.dat', 'folder2/file4.txt',
        'folder3/file5.txt'
    ])

    # Upload again to check that nothing changes.
    res = utils.run_rsync('--files-from', sources_file, self.local_base_dir,
                          self.remote_base_dir)
    self.assertTrue(utils.files_count_is(res, matching=4, extraneous_dir=3))

  def test_checksum_file(self):
    """Uploads and syncs a file with --checksum.

       1) Uploads a file.
       2) Uploads a file with --checksum option. As the file was not changed, it
       is recognized as matched. The output should contain D100%.
       3) Uploads the same file with --whole-file --checksum -v.
       Checks the output contains C100%, not D100%.
       4) Modifies the file without changing its content. The file is
       synchronized, the output should contain D100%.

    """

    utils.create_test_file(self.local_data_path, 1024)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir)

    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          '--checksum', '-v')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, matching=1))
    self.assertIn('D100%', str(res.stdout))
    self.assertIn('will be synced due to -c/--checksum', str(res.stdout))

    utils.create_test_file(self.local_data_path, 2534)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          '--checksum', '-v', '--whole-file')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    self.assertIn('C100%', str(res.stdout))
    self.assertIn('will be copied due to -c/--checksum and -W/--whole-file',
                  str(res.stdout))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))

    utils.change_modified_time(self.local_data_path)
    res = utils.run_rsync(self.local_data_path, self.remote_base_dir, '-c',
                          '-v')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, changed=1))
    self.assertIn('D100%', str(res.stdout))

  def test_sync_folder_when_remote_file_non_recursive(self):
    """Non-recursively uploads a folder while there is a remote file with the same name."""

    local_folder = self.local_base_dir + 'foldertocopy\\'
    utils.create_test_directory(local_folder)
    utils.get_ssh_command_output(
        'mkdir -p %s && touch %s' %
        (self.remote_base_dir, self.remote_base_dir + 'foldertocopy'))

    res = utils.run_rsync(self.local_base_dir + 'foldertocopy',
                          self.remote_base_dir)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, extraneous=1))
    self.assertFalse(
        utils.does_directory_exist_remotely(self.remote_base_dir +
                                            'foldertocopy'))
    self.assertTrue(
        utils.does_file_exist_remotely(self.remote_base_dir + 'foldertocopy'))

  def test_sync_folder_when_remote_file_recursive_with_delete(self):
    """Recursively uploads a folder while removing a remote file with the same name with --delete."""

    local_folder = self.local_base_dir + 'foldertocopy\\'
    utils.create_test_directory(local_folder)
    utils.get_ssh_command_output(
        'mkdir -p %s && touch %s' %
        (self.remote_base_dir, self.remote_base_dir + 'foldertocopy'))

    res = utils.run_rsync(self.local_base_dir + 'foldertocopy',
                          self.remote_base_dir, '-r', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, extraneous=1, missing_dir=1))
    self.assertTrue(
        utils.does_directory_exist_remotely(self.remote_base_dir +
                                            'foldertocopy'))
    self.assertFalse(
        utils.does_file_exist_remotely(self.remote_base_dir + 'foldertocopy'))
    self.assertIn('1/1 file(s) and 0/0 folder(s) deleted', str(res.stdout))

  def test_sync_file_when_remote_folder_recursive_with_delete(self):
    """Recursively uploads a file while removing a remote folder with the same name with --delete."""
    utils.create_test_file(self.local_data_path, 1024)
    utils.get_ssh_command_output('mkdir -p %s' % self.remote_data_path)

    res = utils.run_rsync(self.local_data_path, self.remote_base_dir,
                          '--delete', '-r')
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=1, extraneous_dir=1))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))
    self.assertFalse(utils.does_directory_exist_remotely(self.remote_data_path))
    self.assertIn('0/0 file(s) and 1/1 folder(s) deleted', str(res.stdout))

  def test_sync_file_when_remote_folder_empty_non_recursive(self):
    """Non-recursively uploads a file while there is an empty remote folder with the same name."""
    self._do_test_sync_file_when_remote_folder_empty(recursive=False)

  def test_sync_file_when_remote_folder_empty_recursive(self):
    """Recursively uploads a file while there is an empty remote folder with the same name."""
    self._do_test_sync_file_when_remote_folder_empty(recursive=True)

  def _do_test_sync_file_when_remote_folder_empty(self, recursive):
    """Uploads a file while there is an empty remote folder with the same name.

    Args:
        recursive (bool): Whether to append '-r' or not.
    """
    flag = '-r' if recursive else None
    utils.create_test_file(self.local_data_path, 1024)
    utils.get_ssh_command_output('mkdir -p %s' % self.remote_data_path)

    res = utils.run_rsync(self.local_data_path, self.remote_base_dir, flag)
    self._assert_rsync_success(res)
    self.assertTrue(utils.files_count_is(res, missing=1, extraneous_dir=1))
    self.assertTrue(
        utils.sha1_matches(self.local_data_path, self.remote_data_path))
    self.assertFalse(utils.does_directory_exist_remotely(self.remote_data_path))
    self.assertNotIn('0/0 file(s) and 1/1 folder(s) deleted', str(res.stdout))

  def test_sync_file_when_remote_folder_non_empty_non_recursive(self):
    """Non-recursively uploads a file while there is a non-empty remote folder with the same name."""
    self._do_test_sync_file_when_remote_folder_non_empty(recursive=False)

  def test_sync_file_when_remote_folder_non_empty_recursive(self):
    """Recursively uploads a file while there is a non-empty remote folder with the same name."""
    self._do_test_sync_file_when_remote_folder_non_empty(recursive=True)

  def _do_test_sync_file_when_remote_folder_non_empty(self, recursive):
    """Uploads a file while there is a non-empty remote folder with the same name.

    Args:
        recursive (bool): Whether to append '-r' or not.
    """
    flag = '-r' if recursive else None
    utils.create_test_file(self.local_data_path, 1024)
    utils.get_ssh_command_output('mkdir -p %s' % self.remote_data_path)
    utils.get_ssh_command_output(
        'mkdir -p %s && touch %s' %
        (self.remote_base_dir, self.remote_data_path + '/file1.txt'))

    res = utils.run_rsync(self.local_data_path, self.remote_base_dir, flag)
    self.assertIn('remove() failed: Directory not empty.', str(res.stderr))
    if recursive:
      self.assertTrue(
          utils.files_count_is(res, missing=1, extraneous=1, extraneous_dir=1))
    else:
      self.assertTrue(utils.files_count_is(res, missing=1, extraneous_dir=1))
    self.assertTrue(utils.does_directory_exist_remotely(self.remote_data_path))
    self.assertTrue(
        utils.does_file_exist_remotely(self.remote_data_path + '/file1.txt'))
    self.assertFalse(utils.does_file_exist_remotely(self.remote_data_path))

  def test_upload_from_dot(self):
    """Uploads files from the current directory ('.')."""
    utils.create_test_file(self.local_base_dir + 'file1.txt', 1024)
    utils.create_test_file(self.local_base_dir + 'dir\\file2.txt', 1024)

    prev_cwd = os.getcwd()
    os.chdir(self.local_base_dir)
    try:
      # Uploading recursivly should pick up all files and dirs.
      res = utils.run_rsync('.', self.remote_base_dir, '-r')
      self.assertTrue(utils.files_count_is(res, missing=2, missing_dir=1))
      self._assert_remote_dir_contains(['file1.txt', 'dir/file2.txt'])

      # Uploading again should not change anything.
      res = utils.run_rsync('.', self.remote_base_dir, '-r')
      self.assertTrue(utils.files_count_is(res, matching=2, matching_dir=1))

      # Verify that non-recursive uploads do nothing.
      res = utils.run_rsync('.', self.remote_base_dir)
      self.assertTrue(utils.files_count_is(res, extraneous=1, extraneous_dir=1))
    finally:
      os.chdir(prev_cwd)

  def test_upload_from_dotdot(self):
    """Uploads files from the parent directory ('..')."""
    utils.create_test_file(self.local_base_dir + 'file1.txt', 1024)
    utils.create_test_file(self.local_base_dir + 'dir\\file2.txt', 1024)

    prev_cwd = os.getcwd()
    os.chdir(self.local_base_dir + 'dir')
    try:
      # Uploading recursivly should pick up all files and dirs.
      res = utils.run_rsync('..', self.remote_base_dir, '-r')
      self.assertTrue(utils.files_count_is(res, missing=2, missing_dir=1))
      self._assert_remote_dir_contains(['file1.txt', 'dir/file2.txt'])

      # Uploading again should not change anything.
      res = utils.run_rsync('..', self.remote_base_dir, '-r')
      self.assertTrue(utils.files_count_is(res, matching=2, matching_dir=1))

      # Verify that non-recursive uploads do nothing.
      res = utils.run_rsync('..', self.remote_base_dir)
      self.assertTrue(utils.files_count_is(res, extraneous=1, extraneous_dir=1))
    finally:
      os.chdir(prev_cwd)

  def test_existing(self):
    """Runs rsync with --existing for a non-trivial directory.

       1) Uploads a source directory with -r.
       |-- rootdir
       |   |-- dir1
       |      |-- emptydir2
       |      |-- file1_1.txt
       |      |-- file1_2.txt -> rename to file1_3.txt (step 2)
       |      |-- (step2) emptydir3
       |   |-- dir2
       |      |-- file2_1.txt
       |   |-- emptydir1      -> rename emptydir4 (step 2)
       |   |-- file0.txt      -> change (step 2)
       2) Add new files/folders, remove and change some files/folders.
       3) Uploads the same source directory with --existing option and with -r.
       Only files existing on the server are changed, nothing is removed.
       4) Uploads the same source directory with --existing --delete -r.
       Files non-existing on the server are deleted.
    """
    local_root_path = self.local_base_dir + 'rootdir'
    remote_root_path = self.remote_base_dir + 'rootdir/'

    files = [
        '\\dir1\\file1_1.txt', '\\dir1\\file1_2.txt', '\\dir2\\file2_1.txt',
        '\\file0.txt'
    ]
    for file in files:
      utils.create_test_file(local_root_path + file, 1024)
    dirs = ['\\dir1\\emptydir2\\', '\\emptydir1\\']
    for directory in dirs:
      utils.create_test_directory(local_root_path + directory)
    res = utils.run_rsync(local_root_path, self.remote_base_dir, '-r')
    self._assert_rsync_success(res)

    utils.remove_test_file(local_root_path + '\\dir1\\file1_2.txt')
    utils.create_test_file(local_root_path + '\\dir1\\file1_3.txt', 1024)
    utils.create_test_directory(local_root_path + '\\dir1\\emptydir3\\')
    utils.remove_test_directory(local_root_path + '\\emptydir1\\')
    utils.create_test_directory(local_root_path + '\\emptydir4\\')
    utils.create_test_file(local_root_path + '\\file0.txt', 2034)

    res = utils.run_rsync(local_root_path, self.remote_base_dir, '-r',
                          '--existing')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(
            res,
            missing=1,
            missing_dir=2,
            matching=2,
            matching_dir=4,
            changed=1,
            extraneous=1,
            extraneous_dir=1))
    self.assertTrue(
        utils.does_directory_exist_remotely(remote_root_path + 'emptydir1'))
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'emptydir4'))
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path +
                                            'dir1/emptydir3'))
    self.assertTrue(
        utils.does_file_exist_remotely(remote_root_path + 'dir1/file1_2.txt'))
    self.assertFalse(
        utils.does_file_exist_remotely(remote_root_path + 'dir1/file1_3.txt'))

    res = utils.run_rsync(local_root_path, self.remote_base_dir, '-r',
                          '--existing', '--delete')
    self._assert_rsync_success(res)
    self.assertTrue(
        utils.files_count_is(
            res,
            missing=1,
            missing_dir=2,
            matching=3,
            matching_dir=4,
            extraneous=1,
            extraneous_dir=1))
    self.assertIn('1/1 file(s) and 1/1 folder(s) deleted', res.stdout)
    self.assertFalse(
        utils.does_directory_exist_remotely(remote_root_path + 'emptydir1'))
    self.assertFalse(
        utils.does_file_exist_remotely(remote_root_path + 'dir2/file1_2.txt'))

  def test_copy_dest(self):
    r"""Runs rsync with --copy-dest option.

       Copies testdata.dat to 
       Copies the "cdc_rsync_e2e_test" package locally and syncs it with
       --copy-dest. Verifies that the files are actually sync'ed (D), not
       copied (C).

    Raises:
        Exception: On timeout waiting for mount to appear (after 20 seconds)
    """

    copy_dest_dir = self.remote_base_dir + 'copy_dest_dir'

    utils.create_test_file(self.local_data_path, 1024)
    res = utils.run_rsync(self.local_data_path, copy_dest_dir)
    self._assert_rsync_success(res)

    # Upload package using --package.
    res = utils.run_rsync('--copy-dest', copy_dest_dir, self.local_data_path,
                          self.remote_base_dir, '-v')
    self._assert_rsync_success(res)
    self.assertIn('D100%', res.stdout)
    self.assertNotIn('C100%', res.stdout)

  def test_upload_executables(self):
    """Uploads executable files and checks that they have the x bit set."""

    # Use the cdc rsync binaries as test executables.
    local_exe_path = utils.CDC_RSYNC_PATH
    local_elf_path = os.path.join(
        os.path.dirname(local_exe_path), 'cdc_rsync_server')

    remote_exe_path = self.remote_base_dir + os.path.basename(local_exe_path)
    remote_elf_path = self.remote_base_dir + os.path.basename(local_elf_path)

    # Copy the files to the gamelet.
    res = utils.run_rsync(local_exe_path, local_elf_path, self.remote_base_dir)
    self._assert_rsync_success(res)

    # Check that both files have the executable bit set.
    stats = utils.get_ssh_command_output('stat -c "%%a" %s %s' %
                                         (remote_exe_path, remote_elf_path))
    self.assertEqual(stats.count('755'), 2, stats)

    # Remove executable bits.
    utils.get_ssh_command_output('chmod -x %s %s' %
                                 (remote_exe_path, remote_elf_path))

    # Sync again, using -c to force a sync.
    res = utils.run_rsync('-c', local_exe_path, local_elf_path,
                          self.remote_base_dir)
    self._assert_rsync_success(res)

    # Validate that the executable bits were restored.
    stats = utils.get_ssh_command_output('stat -c "%%a" %s %s' %
                                         (remote_exe_path, remote_elf_path))
    self.assertEqual(stats.count('755'), 2, stats)

  def _run(self, args):
    logging.debug('Running %s', ' '.join(args))
    res = subprocess.run(args, capture_output=True)
    self.assertEqual(res.returncode, 0, 'Command failed: ' + str(res))
    res.stdout = res.stdout.decode('ascii')
    logging.debug('\r\n%s', res.stdout)
    return res


if __name__ == '__main__':
  test_base.test_base.main()
