python/mozbuild/mozpack/test/test_copier.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/python/mozbuild/mozpack/test/test_copier.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,464 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +from mozpack.copier import (
     1.9 +    FileCopier,
    1.10 +    FilePurger,
    1.11 +    FileRegistry,
    1.12 +    Jarrer,
    1.13 +)
    1.14 +from mozpack.files import (
    1.15 +    GeneratedFile,
    1.16 +    ExistingFile,
    1.17 +)
    1.18 +from mozpack.mozjar import JarReader
    1.19 +import mozpack.path
    1.20 +import unittest
    1.21 +import mozunit
    1.22 +import os
    1.23 +import stat
    1.24 +from mozpack.errors import ErrorMessage
    1.25 +from mozpack.test.test_files import (
    1.26 +    MockDest,
    1.27 +    MatchTestTemplate,
    1.28 +    TestWithTmpDir,
    1.29 +)
    1.30 +
    1.31 +
    1.32 +class TestFileRegistry(MatchTestTemplate, unittest.TestCase):
    1.33 +    def add(self, path):
    1.34 +        self.registry.add(path, GeneratedFile(path))
    1.35 +
    1.36 +    def do_check(self, pattern, result):
    1.37 +        self.checked = True
    1.38 +        if result:
    1.39 +            self.assertTrue(self.registry.contains(pattern))
    1.40 +        else:
    1.41 +            self.assertFalse(self.registry.contains(pattern))
    1.42 +        self.assertEqual(self.registry.match(pattern), result)
    1.43 +
    1.44 +    def test_file_registry(self):
    1.45 +        self.registry = FileRegistry()
    1.46 +        self.registry.add('foo', GeneratedFile('foo'))
    1.47 +        bar = GeneratedFile('bar')
    1.48 +        self.registry.add('bar', bar)
    1.49 +        self.assertEqual(self.registry.paths(), ['foo', 'bar'])
    1.50 +        self.assertEqual(self.registry['bar'], bar)
    1.51 +
    1.52 +        self.assertRaises(ErrorMessage, self.registry.add, 'foo',
    1.53 +                          GeneratedFile('foo2'))
    1.54 +
    1.55 +        self.assertRaises(ErrorMessage, self.registry.remove, 'qux')
    1.56 +
    1.57 +        self.assertRaises(ErrorMessage, self.registry.add, 'foo/bar',
    1.58 +                          GeneratedFile('foobar'))
    1.59 +        self.assertRaises(ErrorMessage, self.registry.add, 'foo/bar/baz',
    1.60 +                          GeneratedFile('foobar'))
    1.61 +
    1.62 +        self.assertEqual(self.registry.paths(), ['foo', 'bar'])
    1.63 +
    1.64 +        self.registry.remove('foo')
    1.65 +        self.assertEqual(self.registry.paths(), ['bar'])
    1.66 +        self.registry.remove('bar')
    1.67 +        self.assertEqual(self.registry.paths(), [])
    1.68 +
    1.69 +        self.prepare_match_test()
    1.70 +        self.do_match_test()
    1.71 +        self.assertTrue(self.checked)
    1.72 +        self.assertEqual(self.registry.paths(), [
    1.73 +            'bar',
    1.74 +            'foo/bar',
    1.75 +            'foo/baz',
    1.76 +            'foo/qux/1',
    1.77 +            'foo/qux/bar',
    1.78 +            'foo/qux/2/test',
    1.79 +            'foo/qux/2/test2',
    1.80 +        ])
    1.81 +
    1.82 +        self.registry.remove('foo/qux')
    1.83 +        self.assertEqual(self.registry.paths(), ['bar', 'foo/bar', 'foo/baz'])
    1.84 +
    1.85 +        self.registry.add('foo/qux', GeneratedFile('fooqux'))
    1.86 +        self.assertEqual(self.registry.paths(), ['bar', 'foo/bar', 'foo/baz',
    1.87 +                                                 'foo/qux'])
    1.88 +        self.registry.remove('foo/b*')
    1.89 +        self.assertEqual(self.registry.paths(), ['bar', 'foo/qux'])
    1.90 +
    1.91 +        self.assertEqual([f for f, c in self.registry], ['bar', 'foo/qux'])
    1.92 +        self.assertEqual(len(self.registry), 2)
    1.93 +
    1.94 +        self.add('foo/.foo')
    1.95 +        self.assertTrue(self.registry.contains('foo/.foo'))
    1.96 +
    1.97 +    def test_registry_paths(self):
    1.98 +        self.registry = FileRegistry()
    1.99 +
   1.100 +        # Can't add a file if it requires a directory in place of a
   1.101 +        # file we also require.
   1.102 +        self.registry.add('foo', GeneratedFile('foo'))
   1.103 +        self.assertRaises(ErrorMessage, self.registry.add, 'foo/bar',
   1.104 +                          GeneratedFile('foobar'))
   1.105 +
   1.106 +        # Can't add a file if we already have a directory there.
   1.107 +        self.registry.add('bar/baz', GeneratedFile('barbaz'))
   1.108 +        self.assertRaises(ErrorMessage, self.registry.add, 'bar',
   1.109 +                          GeneratedFile('bar'))
   1.110 +
   1.111 +        # Bump the count of things that require bar/ to 2.
   1.112 +        self.registry.add('bar/zot', GeneratedFile('barzot'))
   1.113 +        self.assertRaises(ErrorMessage, self.registry.add, 'bar',
   1.114 +                          GeneratedFile('bar'))
   1.115 +
   1.116 +        # Drop the count of things that require bar/ to 1.
   1.117 +        self.registry.remove('bar/baz')
   1.118 +        self.assertRaises(ErrorMessage, self.registry.add, 'bar',
   1.119 +                          GeneratedFile('bar'))
   1.120 +
   1.121 +        # Drop the count of things that require bar/ to 0.
   1.122 +        self.registry.remove('bar/zot')
   1.123 +        self.registry.add('bar/zot', GeneratedFile('barzot'))
   1.124 +
   1.125 +    def test_required_directories(self):
   1.126 +        self.registry = FileRegistry()
   1.127 +
   1.128 +        self.registry.add('foo', GeneratedFile('foo'))
   1.129 +        self.assertEqual(self.registry.required_directories(), set())
   1.130 +
   1.131 +        self.registry.add('bar/baz', GeneratedFile('barbaz'))
   1.132 +        self.assertEqual(self.registry.required_directories(), {'bar'})
   1.133 +
   1.134 +        self.registry.add('bar/zot', GeneratedFile('barzot'))
   1.135 +        self.assertEqual(self.registry.required_directories(), {'bar'})
   1.136 +
   1.137 +        self.registry.add('bar/zap/zot', GeneratedFile('barzapzot'))
   1.138 +        self.assertEqual(self.registry.required_directories(), {'bar', 'bar/zap'})
   1.139 +
   1.140 +        self.registry.remove('bar/zap/zot')
   1.141 +        self.assertEqual(self.registry.required_directories(), {'bar'})
   1.142 +
   1.143 +        self.registry.remove('bar/baz')
   1.144 +        self.assertEqual(self.registry.required_directories(), {'bar'})
   1.145 +
   1.146 +        self.registry.remove('bar/zot')
   1.147 +        self.assertEqual(self.registry.required_directories(), set())
   1.148 +
   1.149 +        self.registry.add('x/y/z', GeneratedFile('xyz'))
   1.150 +        self.assertEqual(self.registry.required_directories(), {'x', 'x/y'})
   1.151 +
   1.152 +
   1.153 +class TestFileCopier(TestWithTmpDir):
   1.154 +    def all_dirs(self, base):
   1.155 +        all_dirs = set()
   1.156 +        for root, dirs, files in os.walk(base):
   1.157 +            if not dirs:
   1.158 +                all_dirs.add(mozpack.path.relpath(root, base))
   1.159 +        return all_dirs
   1.160 +
   1.161 +    def all_files(self, base):
   1.162 +        all_files = set()
   1.163 +        for root, dirs, files in os.walk(base):
   1.164 +            for f in files:
   1.165 +                all_files.add(
   1.166 +                    mozpack.path.join(mozpack.path.relpath(root, base), f))
   1.167 +        return all_files
   1.168 +
   1.169 +    def test_file_copier(self):
   1.170 +        copier = FileCopier()
   1.171 +        copier.add('foo/bar', GeneratedFile('foobar'))
   1.172 +        copier.add('foo/qux', GeneratedFile('fooqux'))
   1.173 +        copier.add('foo/deep/nested/directory/file', GeneratedFile('fooz'))
   1.174 +        copier.add('bar', GeneratedFile('bar'))
   1.175 +        copier.add('qux/foo', GeneratedFile('quxfoo'))
   1.176 +        copier.add('qux/bar', GeneratedFile(''))
   1.177 +
   1.178 +        result = copier.copy(self.tmpdir)
   1.179 +        self.assertEqual(self.all_files(self.tmpdir), set(copier.paths()))
   1.180 +        self.assertEqual(self.all_dirs(self.tmpdir),
   1.181 +                         set(['foo/deep/nested/directory', 'qux']))
   1.182 +
   1.183 +        self.assertEqual(result.updated_files, set(self.tmppath(p) for p in
   1.184 +            self.all_files(self.tmpdir)))
   1.185 +        self.assertEqual(result.existing_files, set())
   1.186 +        self.assertEqual(result.removed_files, set())
   1.187 +        self.assertEqual(result.removed_directories, set())
   1.188 +
   1.189 +        copier.remove('foo')
   1.190 +        copier.add('test', GeneratedFile('test'))
   1.191 +        result = copier.copy(self.tmpdir)
   1.192 +        self.assertEqual(self.all_files(self.tmpdir), set(copier.paths()))
   1.193 +        self.assertEqual(self.all_dirs(self.tmpdir), set(['qux']))
   1.194 +        self.assertEqual(result.removed_files, set(self.tmppath(p) for p in
   1.195 +            ('foo/bar', 'foo/qux', 'foo/deep/nested/directory/file')))
   1.196 +
   1.197 +    def test_symlink_directory_replaced(self):
   1.198 +        """Directory symlinks in destination are replaced if they need to be
   1.199 +        real directories."""
   1.200 +        if not self.symlink_supported:
   1.201 +            return
   1.202 +
   1.203 +        dest = self.tmppath('dest')
   1.204 +
   1.205 +        copier = FileCopier()
   1.206 +        copier.add('foo/bar/baz', GeneratedFile('foobarbaz'))
   1.207 +
   1.208 +        os.makedirs(self.tmppath('dest/foo'))
   1.209 +        dummy = self.tmppath('dummy')
   1.210 +        os.mkdir(dummy)
   1.211 +        link = self.tmppath('dest/foo/bar')
   1.212 +        os.symlink(dummy, link)
   1.213 +
   1.214 +        result = copier.copy(dest)
   1.215 +
   1.216 +        st = os.lstat(link)
   1.217 +        self.assertFalse(stat.S_ISLNK(st.st_mode))
   1.218 +        self.assertTrue(stat.S_ISDIR(st.st_mode))
   1.219 +
   1.220 +        self.assertEqual(self.all_files(dest), set(copier.paths()))
   1.221 +
   1.222 +        self.assertEqual(result.removed_directories, set())
   1.223 +        self.assertEqual(len(result.updated_files), 1)
   1.224 +
   1.225 +    def test_remove_unaccounted_directory_symlinks(self):
   1.226 +        """Directory symlinks in destination that are not in the way are
   1.227 +        deleted according to remove_unaccounted and
   1.228 +        remove_all_directory_symlinks.
   1.229 +        """
   1.230 +        if not self.symlink_supported:
   1.231 +            return
   1.232 +
   1.233 +        dest = self.tmppath('dest')
   1.234 +
   1.235 +        copier = FileCopier()
   1.236 +        copier.add('foo/bar/baz', GeneratedFile('foobarbaz'))
   1.237 +
   1.238 +        os.makedirs(self.tmppath('dest/foo'))
   1.239 +        dummy = self.tmppath('dummy')
   1.240 +        os.mkdir(dummy)
   1.241 +
   1.242 +        os.mkdir(self.tmppath('dest/zot'))
   1.243 +        link = self.tmppath('dest/zot/zap')
   1.244 +        os.symlink(dummy, link)
   1.245 +
   1.246 +        # If not remove_unaccounted but remove_empty_directories, then
   1.247 +        # the symlinked directory remains (as does its containing
   1.248 +        # directory).
   1.249 +        result = copier.copy(dest, remove_unaccounted=False,
   1.250 +            remove_empty_directories=True,
   1.251 +            remove_all_directory_symlinks=False)
   1.252 +
   1.253 +        st = os.lstat(link)
   1.254 +        self.assertTrue(stat.S_ISLNK(st.st_mode))
   1.255 +        self.assertFalse(stat.S_ISDIR(st.st_mode))
   1.256 +
   1.257 +        self.assertEqual(self.all_files(dest), set(copier.paths()))
   1.258 +        self.assertEqual(self.all_dirs(dest), set(['foo/bar']))
   1.259 +
   1.260 +        self.assertEqual(result.removed_directories, set())
   1.261 +        self.assertEqual(len(result.updated_files), 1)
   1.262 +
   1.263 +        # If remove_unaccounted but not remove_empty_directories, then
   1.264 +        # only the symlinked directory is removed.
   1.265 +        result = copier.copy(dest, remove_unaccounted=True,
   1.266 +            remove_empty_directories=False,
   1.267 +            remove_all_directory_symlinks=False)
   1.268 +
   1.269 +        st = os.lstat(self.tmppath('dest/zot'))
   1.270 +        self.assertFalse(stat.S_ISLNK(st.st_mode))
   1.271 +        self.assertTrue(stat.S_ISDIR(st.st_mode))
   1.272 +
   1.273 +        self.assertEqual(result.removed_files, set([link]))
   1.274 +        self.assertEqual(result.removed_directories, set())
   1.275 +
   1.276 +        self.assertEqual(self.all_files(dest), set(copier.paths()))
   1.277 +        self.assertEqual(self.all_dirs(dest), set(['foo/bar', 'zot']))
   1.278 +
   1.279 +        # If remove_unaccounted and remove_empty_directories, then
   1.280 +        # both the symlink and its containing directory are removed.
   1.281 +        link = self.tmppath('dest/zot/zap')
   1.282 +        os.symlink(dummy, link)
   1.283 +
   1.284 +        result = copier.copy(dest, remove_unaccounted=True,
   1.285 +            remove_empty_directories=True,
   1.286 +            remove_all_directory_symlinks=False)
   1.287 +
   1.288 +        self.assertEqual(result.removed_files, set([link]))
   1.289 +        self.assertEqual(result.removed_directories, set([self.tmppath('dest/zot')]))
   1.290 +
   1.291 +        self.assertEqual(self.all_files(dest), set(copier.paths()))
   1.292 +        self.assertEqual(self.all_dirs(dest), set(['foo/bar']))
   1.293 +
   1.294 +    def test_permissions(self):
   1.295 +        """Ensure files without write permission can be deleted."""
   1.296 +        with open(self.tmppath('dummy'), 'a'):
   1.297 +            pass
   1.298 +
   1.299 +        p = self.tmppath('no_perms')
   1.300 +        with open(p, 'a'):
   1.301 +            pass
   1.302 +
   1.303 +        # Make file and directory unwritable. Reminder: making a directory
   1.304 +        # unwritable prevents modifications (including deletes) from the list
   1.305 +        # of files in that directory.
   1.306 +        os.chmod(p, 0400)
   1.307 +        os.chmod(self.tmpdir, 0400)
   1.308 +
   1.309 +        copier = FileCopier()
   1.310 +        copier.add('dummy', GeneratedFile('content'))
   1.311 +        result = copier.copy(self.tmpdir)
   1.312 +        self.assertEqual(result.removed_files_count, 1)
   1.313 +        self.assertFalse(os.path.exists(p))
   1.314 +
   1.315 +    def test_no_remove(self):
   1.316 +        copier = FileCopier()
   1.317 +        copier.add('foo', GeneratedFile('foo'))
   1.318 +
   1.319 +        with open(self.tmppath('bar'), 'a'):
   1.320 +            pass
   1.321 +
   1.322 +        os.mkdir(self.tmppath('emptydir'))
   1.323 +        d = self.tmppath('populateddir')
   1.324 +        os.mkdir(d)
   1.325 +
   1.326 +        with open(self.tmppath('populateddir/foo'), 'a'):
   1.327 +            pass
   1.328 +
   1.329 +        result = copier.copy(self.tmpdir, remove_unaccounted=False)
   1.330 +
   1.331 +        self.assertEqual(self.all_files(self.tmpdir), set(['foo', 'bar',
   1.332 +            'populateddir/foo']))
   1.333 +        self.assertEqual(self.all_dirs(self.tmpdir), set(['populateddir']))
   1.334 +        self.assertEqual(result.removed_files, set())
   1.335 +        self.assertEqual(result.removed_directories,
   1.336 +            set([self.tmppath('emptydir')]))
   1.337 +
   1.338 +    def test_no_remove_empty_directories(self):
   1.339 +        copier = FileCopier()
   1.340 +        copier.add('foo', GeneratedFile('foo'))
   1.341 +
   1.342 +        with open(self.tmppath('bar'), 'a'):
   1.343 +            pass
   1.344 +
   1.345 +        os.mkdir(self.tmppath('emptydir'))
   1.346 +        d = self.tmppath('populateddir')
   1.347 +        os.mkdir(d)
   1.348 +
   1.349 +        with open(self.tmppath('populateddir/foo'), 'a'):
   1.350 +            pass
   1.351 +
   1.352 +        result = copier.copy(self.tmpdir, remove_unaccounted=False,
   1.353 +            remove_empty_directories=False)
   1.354 +
   1.355 +        self.assertEqual(self.all_files(self.tmpdir), set(['foo', 'bar',
   1.356 +            'populateddir/foo']))
   1.357 +        self.assertEqual(self.all_dirs(self.tmpdir), set(['emptydir',
   1.358 +            'populateddir']))
   1.359 +        self.assertEqual(result.removed_files, set())
   1.360 +        self.assertEqual(result.removed_directories, set())
   1.361 +
   1.362 +    def test_optional_exists_creates_unneeded_directory(self):
   1.363 +        """Demonstrate that a directory not strictly required, but specified
   1.364 +        as the path to an optional file, will be unnecessarily created.
   1.365 +
   1.366 +        This behaviour is wrong; fixing it is tracked by Bug 972432;
   1.367 +        and this test exists to guard against unexpected changes in
   1.368 +        behaviour.
   1.369 +        """
   1.370 +
   1.371 +        dest = self.tmppath('dest')
   1.372 +
   1.373 +        copier = FileCopier()
   1.374 +        copier.add('foo/bar', ExistingFile(required=False))
   1.375 +
   1.376 +        result = copier.copy(dest)
   1.377 +
   1.378 +        st = os.lstat(self.tmppath('dest/foo'))
   1.379 +        self.assertFalse(stat.S_ISLNK(st.st_mode))
   1.380 +        self.assertTrue(stat.S_ISDIR(st.st_mode))
   1.381 +
   1.382 +        # What's worse, we have no record that dest was created.
   1.383 +        self.assertEquals(len(result.updated_files), 0)
   1.384 +
   1.385 +        # But we do have an erroneous record of an optional file
   1.386 +        # existing when it does not.
   1.387 +        self.assertIn(self.tmppath('dest/foo/bar'), result.existing_files)
   1.388 +
   1.389 +
   1.390 +class TestFilePurger(TestWithTmpDir):
   1.391 +    def test_file_purger(self):
   1.392 +        existing = os.path.join(self.tmpdir, 'existing')
   1.393 +        extra = os.path.join(self.tmpdir, 'extra')
   1.394 +        empty_dir = os.path.join(self.tmpdir, 'dir')
   1.395 +
   1.396 +        with open(existing, 'a'):
   1.397 +            pass
   1.398 +
   1.399 +        with open(extra, 'a'):
   1.400 +            pass
   1.401 +
   1.402 +        os.mkdir(empty_dir)
   1.403 +        with open(os.path.join(empty_dir, 'foo'), 'a'):
   1.404 +            pass
   1.405 +
   1.406 +        self.assertTrue(os.path.exists(existing))
   1.407 +        self.assertTrue(os.path.exists(extra))
   1.408 +
   1.409 +        purger = FilePurger()
   1.410 +        purger.add('existing')
   1.411 +        result = purger.purge(self.tmpdir)
   1.412 +        self.assertEqual(result.removed_files, set(self.tmppath(p) for p in
   1.413 +            ('extra', 'dir/foo')))
   1.414 +        self.assertEqual(result.removed_files_count, 2)
   1.415 +        self.assertEqual(result.removed_directories_count, 1)
   1.416 +
   1.417 +        self.assertTrue(os.path.exists(existing))
   1.418 +        self.assertFalse(os.path.exists(extra))
   1.419 +        self.assertFalse(os.path.exists(empty_dir))
   1.420 +
   1.421 +
   1.422 +class TestJarrer(unittest.TestCase):
   1.423 +    def check_jar(self, dest, copier):
   1.424 +        jar = JarReader(fileobj=dest)
   1.425 +        self.assertEqual([f.filename for f in jar], copier.paths())
   1.426 +        for f in jar:
   1.427 +            self.assertEqual(f.uncompressed_data.read(),
   1.428 +                             copier[f.filename].content)
   1.429 +
   1.430 +    def test_jarrer(self):
   1.431 +        copier = Jarrer()
   1.432 +        copier.add('foo/bar', GeneratedFile('foobar'))
   1.433 +        copier.add('foo/qux', GeneratedFile('fooqux'))
   1.434 +        copier.add('foo/deep/nested/directory/file', GeneratedFile('fooz'))
   1.435 +        copier.add('bar', GeneratedFile('bar'))
   1.436 +        copier.add('qux/foo', GeneratedFile('quxfoo'))
   1.437 +        copier.add('qux/bar', GeneratedFile(''))
   1.438 +
   1.439 +        dest = MockDest()
   1.440 +        copier.copy(dest)
   1.441 +        self.check_jar(dest, copier)
   1.442 +
   1.443 +        copier.remove('foo')
   1.444 +        copier.add('test', GeneratedFile('test'))
   1.445 +        copier.copy(dest)
   1.446 +        self.check_jar(dest, copier)
   1.447 +
   1.448 +        copier.remove('test')
   1.449 +        copier.add('test', GeneratedFile('replaced-content'))
   1.450 +        copier.copy(dest)
   1.451 +        self.check_jar(dest, copier)
   1.452 +
   1.453 +        copier.copy(dest)
   1.454 +        self.check_jar(dest, copier)
   1.455 +
   1.456 +        preloaded = ['qux/bar', 'bar']
   1.457 +        copier.preload(preloaded)
   1.458 +        copier.copy(dest)
   1.459 +
   1.460 +        dest.seek(0)
   1.461 +        jar = JarReader(fileobj=dest)
   1.462 +        self.assertEqual([f.filename for f in jar], preloaded +
   1.463 +                         [p for p in copier.paths() if not p in preloaded])
   1.464 +        self.assertEqual(jar.last_preloaded, preloaded[-1])
   1.465 +
   1.466 +if __name__ == '__main__':
   1.467 +    mozunit.main()

mercurial