Skip to content
Snippets Groups Projects
cheap-copy.py 5.27 KiB
#!/usr/bin/env python3
# Copyright 2020 Collabora Ltd.
#
# SPDX-License-Identifier: MIT

import os
import subprocess
import sys
import tempfile


try:
    import typing
    typing      # placate pyflakes
except ImportError:
    pass

from testutils import (
    BaseTest,
    test_main,
)


class TestCheapCopy(BaseTest):
    def setUp(self) -> None:
        super().setUp()
        self.cheap_copy = os.path.join(self.G_TEST_BUILDDIR, 'test-cheap-copy')

    def assert_tree_is_superset(
        self,
        superset,
        subset,
        require_hard_links: bool = True,
    ):
        for path, dirs, files in os.walk(subset):
            equivalent = os.path.join(superset, os.path.relpath(path, subset))

            for d in dirs:
                in_subset = os.path.join(path, d)
                in_superset = os.path.join(equivalent, d)

                if not os.path.isdir(in_superset):
                    raise AssertionError(
                        '%r should be a directory', in_superset)

                info = os.stat(in_subset)
                info2 = os.stat(in_superset)
                self.assertEqual(info.st_mode, info2.st_mode)

            for f in files:
                in_subset = os.path.join(path, f)
                in_superset = os.path.join(equivalent, f)

                if (
                    os.path.islink(in_subset)
                    or not os.path.exists(in_subset)
                ):
                    target = os.readlink(in_subset)
                    target2 = os.readlink(in_superset)
                    self.assertEqual(target, target2)
                else:
                    info = os.stat(in_subset)
                    info2 = os.stat(in_superset)

                    if require_hard_links:
                        self.assertEqual(info.st_ino, info2.st_ino)
                        self.assertEqual(info.st_dev, info2.st_dev)

                    self.assertEqual(info.st_mode, info2.st_mode)
                    self.assertEqual(info.st_size, info2.st_size)
                    self.assertEqual(int(info.st_mtime), int(info2.st_mtime))
                    self.assertEqual(int(info.st_ctime), int(info2.st_ctime))

    def assert_tree_is_same(self, left, right, require_hard_links=True):
        self.assert_tree_is_superset(left, right, require_hard_links)
        self.assert_tree_is_superset(right, left, require_hard_links)

    def test_empty(self) -> None:
        with tempfile.TemporaryDirectory(
        ) as source, tempfile.TemporaryDirectory(
        ) as dest:
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
            )
            self.assert_tree_is_same(source, dest)

    def test_create(self) -> None:
        with tempfile.TemporaryDirectory(
        ) as source, tempfile.TemporaryDirectory(
        ) as parent:
            dest = os.path.join(parent, 'dest')
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
            )
            self.assert_tree_is_same(source, dest)

    def test_populated(
        self,
        dir1=None,      # type: typing.Optional[str]
        dir2=None,      # type: typing.Optional[str]
        require_hard_links=True,
    ) -> None:
        with tempfile.TemporaryDirectory(
            dir=dir1,
        ) as source, tempfile.TemporaryDirectory(
            dir=dir2,
        ) as parent:
            os.makedirs(os.path.join(source, 'a', 'b', 'c'))
            os.makedirs(os.path.join(source, 'files'))

            with open(os.path.join(source, 'x'), 'w') as writer:
                writer.write('hello')

            # Use unusual (but safe) permissions to assert that they get
            # copied
            os.chmod(source, 0o2740)
            os.chmod(os.path.join(source, 'x'), 0o651)

            with open(os.path.join(source, 'files', 'y'), 'w') as writer:
                writer.write('hello')

            os.symlink('y', os.path.join(source, 'files', 'exists'))
            os.symlink('/dev', os.path.join(source, 'files', 'dev'))
            os.symlink('no', os.path.join(source, 'files', 'not here'))

            dest = os.path.join(parent, 'dest')
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
            )
            self.assert_tree_is_same(source, dest, require_hard_links)

    def test_cannot_hard_link(self):
        """
        Assert that we can copy a directory hierarchy between directories
        that might be on different filesystems.

        If /tmp and /var/tmp are both on the same mount point, this is
        equivalent to test_populated(), but if they are on different
        mount points (/tmp is often a tmpfs) then this exercises different
        code paths.
        """
        self.test_populated('/tmp', '/var/tmp', require_hard_links=False)

    def tearDown(self) -> None:
        super().tearDown()


if __name__ == '__main__':
    assert sys.version_info >= (3, 5), \
        'Python 3.5+ is required (configure with -Dpython=python3.5 ' \
        'if necessary)'

    test_main()

# vi: set sw=4 sts=4 et: