Skip to content
Snippets Groups Projects
  • Simon McVittie's avatar
    16e20ba0
    mtree: Parse the ignore, nochange and optional flags · 16e20ba0
    Simon McVittie authored
    
    The nochange flag is fully implemented here: it suppresses changing the
    modification time or permissions.
    
    The optional flag is mostly implemented here: it suppresses the error
    that would ordinarily occur if a file with content does not already
    exist.
    
    The ignore flag is not yet implemented, only parsed, because it only
    really applies when verifying whether a filesystem tree matches a
    specification.
    
    Signed-off-by: default avatarSimon McVittie <smcv@collabora.com>
    16e20ba0
    History
    mtree: Parse the ignore, nochange and optional flags
    Simon McVittie authored
    
    The nochange flag is fully implemented here: it suppresses changing the
    modification time or permissions.
    
    The optional flag is mostly implemented here: it suppresses the error
    that would ordinarily occur if a file with content does not already
    exist.
    
    The ignore flag is not yet implemented, only parsed, because it only
    really applies when verifying whether a filesystem tree matches a
    specification.
    
    Signed-off-by: default avatarSimon McVittie <smcv@collabora.com>
mtree-apply.py 9.08 KiB
#!/usr/bin/env python3
# Copyright 2021 Collabora Ltd.
#
# SPDX-License-Identifier: MIT

import os
import subprocess
import sys
import tempfile
from pathlib import Path


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

from testutils import (
    BaseTest,
    test_main,
)


class TestMtreeApply(BaseTest):
    def setUp(self) -> None:
        super().setUp()
        os.environ['G_MESSAGES_DEBUG'] = 'all'
        self.mtree_apply = os.path.join(
            self.G_TEST_BUILDDIR,
            'test-mtree-apply',
        )

    def assert_tree_is_superset(
        self,
        superset,
        subset,
        require_hard_links: bool = True,
        require_permissions: bool = True,
        require_times: 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(oct(info.st_mode), oct(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)

                    if require_permissions:
                        self.assertEqual(oct(info.st_mode), oct(info2.st_mode))
                    else:
                        self.assertEqual(
                            oct(info.st_mode & ~0o7777),
                            oct(info2.st_mode & ~0o7777),
                        )

                    self.assertEqual(info.st_size, info2.st_size)

                    if require_times:
                        self.assertEqual(
                            int(info.st_mtime),
                            int(info2.st_mtime),
                        )

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

    def test_empty(self) -> None:
        content = b''

        with tempfile.NamedTemporaryFile(
        ) as source, tempfile.TemporaryDirectory(
        ) as expected, tempfile.TemporaryDirectory(
        ) as dest:
            source.write(content)
            source.flush()

            subprocess.run(
                [
                    self.mtree_apply,
                    source.name,
                    dest,
                ],
                check=True,
                stdout=2,
            )
            self.assert_tree_is_same(
                dest,
                expected,
                require_hard_links=False,
                require_permissions=False,
                require_times=False,
            )

    def test_populate(self) -> None:
        content = b'''\
        . type=dir
        ./foo/bar/\302\251 type=dir
        ./sym/link type=link link=/dev/null
        ./create type=file size=0
        ./make-executable type=file mode=755
        ./make-non-executable type=file mode=644 time=1597415889
        ./do-not-assert type=file optional
        ./do-not-chmod type=file mode=755 nochange
        '''

        with tempfile.NamedTemporaryFile(
        ) as source, tempfile.TemporaryDirectory(
        ) as expected, tempfile.TemporaryDirectory(
        ) as dest:
            source.write(content)
            source.flush()

            os.umask(0o077)
            bar = (Path(expected) / 'foo' / 'bar')
            bar.mkdir(parents=True)
            non_ascii_filename = str(bar).encode('ascii') + b'/\302\251'
            os.mkdir(non_ascii_filename)
            (Path(expected) / 'sym').mkdir()
            (Path(expected) / 'sym' / 'link').symlink_to('/dev/null')

            for filename in (
                'make-executable',
                'make-non-executable',
                'do-not-chmod',
            ):
                for d in Path(expected), Path(dest):
                    with open(str(d / filename), 'w') as writer:
                        writer.write('#!/bin/sh\n')

            with open(str(Path(expected) / 'create'), 'w') as writer:
                pass

            # Paths that are not explicitly created get their permissions
            # from umask
            (Path(expected)).chmod(0o755)
            os.chmod(non_ascii_filename, 0o755)
            (Path(expected) / 'create').chmod(0o644)
            (Path(expected) / 'make-executable').chmod(0o755)
            (Path(expected) / 'make-non-executable').chmod(0o644)
            (Path(dest) / 'do-not-chmod').chmod(0o644)
            (Path(expected) / 'do-not-chmod').chmod(0o644)
            os.utime(
                str(Path(expected) / 'make-non-executable'),
                times=(1597415889, 1597415889),
            )

            subprocess.run(
                [
                    self.mtree_apply,
                    source.name,
                    dest,
                ],
                check=True,
                stdout=2,
            )
            subprocess.run(
                [
                    'find',
                    dest,
                    '-ls',
                ],
                check=True,
                stdout=2,
            )
            self.assert_tree_is_same(
                dest,
                expected,
                require_hard_links=False,
                require_permissions=True,
                require_times=False,
            )

    def test_populate_copy(self) -> None:
        content = b'''\
# Content-addressed storage indexed by a truncated sha256
./make-executable type=file mode=755 contents=a8/076d3d28d21e02012b20eaf7dbf754
./make-non-executable type=file mode=644 time=1597415889
'''

        with tempfile.NamedTemporaryFile(
        ) as source, tempfile.TemporaryDirectory(
        ) as reference, tempfile.TemporaryDirectory(
        ) as expected, tempfile.TemporaryDirectory(
        ) as dest:
            source.write(content)
            source.flush()

            os.umask(0o077)

            for filename in 'make-executable', 'make-non-executable':
                with open(str(Path(expected) / filename), 'w') as writer:
                    writer.write('#!/bin/sh\n')

            # Avoid Path.link_to() which wasn't in Python 3.5
            os.link(
                str(Path(expected) / 'make-non-executable'),
                str(Path(reference) / 'make-non-executable'),
            )
            (Path(reference) / 'a8').mkdir()
            os.link(
                str(Path(expected) / 'make-executable'),
                str(
                    Path(reference) / 'a8'
                    / '076d3d28d21e02012b20eaf7dbf754'
                ),
            )

            subprocess.run(
                [
                    self.mtree_apply,
                    source.name,
                    dest,
                    reference,
                ],
                check=True,
                stdout=2,
            )

            subprocess.run(
                [
                    'find',
                    dest,
                    '-ls',
                ],
                check=True,
                stdout=2,
            )
            self.assert_tree_is_same(
                dest,
                expected,
                require_hard_links=True,
                require_permissions=True,
                require_times=True,
            )

            info = (Path(dest) / 'make-executable').stat()
            self.assertEqual(info.st_mode & 0o7777, 0o755)

            info = (Path(dest) / 'make-non-executable').stat()
            self.assertEqual(info.st_mode & 0o7777, 0o644)
            self.assertEqual(info.st_mtime, 1597415889)

    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: