Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
447 lines (397 sloc) 18.3 KB
"""Tests for linkdirs."""
import os
import re
import stat
from io import StringIO
import unittest
import mock
from pyfakefs import fake_filesystem_unittest
import linkdirs
class TestMain(unittest.TestCase):
"""Tests for main()."""
@mock.patch('linkdirs.real_main', return_value=[])
@mock.patch('sys.exit')
# pylint: disable=no-self-use
def test_success(self, mock_sys_exit, unused_mock_real_main):
"""Successful run."""
linkdirs.main([])
mock_sys_exit.assert_called_once_with(0)
@mock.patch('sys.stdout', new_callable=StringIO)
@mock.patch('linkdirs.real_main', return_value=['a message'])
@mock.patch('sys.exit')
def test_failure(self, mock_sys_exit, unused_mock_real_main, mock_stdout):
"""Failed run."""
linkdirs.main([])
self.assertEqual('a message\n', mock_stdout.getvalue())
# In reality sys.exit will only be called once, but because we mock it out
# the flow control continues and it is called twice.
mock_sys_exit.assert_has_calls([mock.call(1), mock.call(0)])
class TestIntegration(fake_filesystem_unittest.TestCase):
"""Integration tests: exercise as much code as possible.
Use cases to test:
- Nothing needs to be done.
- Missing file gets created.
- File with same contents is replaced with link.
- Excluded files/dirs are skipped.
- Report unexpected files.
- Delete unexpected files.
- Report diffs.
- Bad arguments are caught.
- Force deletes existing files and directories.
- Dry-run.
"""
def assert_files_are_linked(self, file1, file2):
"""Assert that two files are linked."""
self.assertTrue(os.path.samefile(file1, file2))
def setUp(self):
self.setUpPyfakefs()
def test_nothing_changes(self):
"""Nothing needs to be done."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_file = '/a/b/c/file'
dest_file = '/z/y/x/file'
self.fs.CreateFile(src_file, contents='qwerty')
os.makedirs(os.path.dirname(dest_file))
os.link(src_file, dest_file)
self.assert_files_are_linked(src_file, dest_file)
linkdirs.real_main(['linkdirs', os.path.dirname(src_file),
os.path.dirname(dest_file)])
self.assert_files_are_linked(src_file, dest_file)
def test_dest_perms_unchanged(self): # pylint: disable=no-self-use
"""Destination directory perms don't change unnecessarily."""
src_dir = '/a/b/c/dir'
dest_dir = '/z/y/x/dir'
mode = int('0755', base=8)
os.makedirs(src_dir)
os.makedirs(dest_dir)
os.chmod(src_dir, mode)
os.chmod(dest_dir, mode)
with mock.patch('os.chmod') as fake_chmod:
linkdirs.real_main(['linkdirs', os.path.dirname(src_dir),
os.path.dirname(dest_dir)])
fake_chmod.assert_not_called()
def test_dest_perms_are_changed(self):
"""Destination directory perms change if necessarily."""
src_dir = '/a/b/c/dir'
dest_dir = '/z/y/x/dir'
mode = int('0755', base=8)
os.makedirs(src_dir)
os.makedirs(dest_dir)
os.chmod(src_dir, mode)
os.chmod(dest_dir, int('0700', base=8))
linkdirs.real_main(['linkdirs', os.path.dirname(src_dir),
os.path.dirname(dest_dir)])
self.assertEqual(mode, stat.S_IMODE(os.stat(dest_dir).st_mode))
def test_missing_file_is_created(self):
"""Missing file gets created."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_file = '/a/b/c/file'
dest_file = '/z/y/x/file'
self.fs.CreateFile(src_file, contents='qwerty')
linkdirs.real_main(['linkdirs', os.path.dirname(src_file),
os.path.dirname(dest_file)])
self.assert_files_are_linked(src_file, dest_file)
def test_replace_same_contents(self):
"""File with same contents is replaced with link."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_file = '/a/b/c/file'
dest_file = '/z/y/x/file'
self.fs.CreateFile(src_file, contents='qwerty')
self.fs.CreateFile(dest_file, contents='qwerty')
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
linkdirs.real_main(['linkdirs', os.path.dirname(src_file),
os.path.dirname(dest_file)])
self.assert_files_are_linked(src_file, dest_file)
expected = ('/a/b/c/file and /z/y/x/file are different files but have the'
' same contents; deleting and linking\n')
self.assertMultiLineEqual(expected, mock_stdout.getvalue())
def test_report_unexpected_files(self):
"""Report unexpected files."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_dir = '/a/b/c'
self.fs.CreateFile(os.path.join(src_dir, 'file'))
# 'asdf' subdir exists here, so it will be checked in destdir.
self.fs.CreateFile(os.path.join(src_dir, 'asdf', 'file'))
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(dest_dir, 'pinky'))
self.fs.CreateFile(os.path.join(dest_dir, 'the_brain'))
# Ensure there is a subdir that should not be reported.
os.makedirs(os.path.join(dest_dir, 'subdir'))
# And also a subdir that will be reported.
os.makedirs(os.path.join(dest_dir, 'asdf', 'report_me'))
actual = linkdirs.real_main(['linkdirs', '--report_unexpected_files',
'--ignore_unexpected_children',
src_dir, dest_dir])
expected = [
'Unexpected directory: /z/y/x/asdf/report_me',
'Unexpected file: /z/y/x/pinky',
'Unexpected file: /z/y/x/the_brain',
'rm /z/y/x/pinky /z/y/x/the_brain',
'rmdir /z/y/x/asdf/report_me',
]
self.assertEqual(expected, actual)
# Unexpected files should not be deleted.
self.assertTrue(os.path.exists('/z/y/x/the_brain'))
def test_delete_unexpected_files(self):
"""Delete unexpected files."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_dir = '/a/b/c'
self.fs.CreateFile(os.path.join(src_dir, 'file'))
# 'asdf' subdir exists here, so it will be checked in destdir.
self.fs.CreateFile(os.path.join(src_dir, 'asdf', 'file'))
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(dest_dir, 'pinky'))
self.fs.CreateFile(os.path.join(dest_dir, 'the_brain'))
# Ensure there is a subdir that should not be reported.
os.makedirs(os.path.join(dest_dir, 'subdir'))
actual = linkdirs.real_main(['linkdirs', '--delete_unexpected_files',
'--ignore_unexpected_children',
src_dir, dest_dir])
self.assertEqual([], actual)
# Unexpected files should be deleted.
self.assertFalse(os.path.exists('/z/y/x/the_brain'))
self.assertFalse(os.path.exists('/z/y/x/pinky'))
def test_delete_unexp_keeps_dirs(self):
"""Delete unexpected files but not directories."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_dir = '/a/b/c'
self.fs.CreateFile(os.path.join(src_dir, 'file'))
# 'asdf' subdir exists here, so it will be checked in destdir.
self.fs.CreateFile(os.path.join(src_dir, 'asdf', 'file'))
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(dest_dir, 'pinky'))
self.fs.CreateFile(os.path.join(dest_dir, 'the_brain'))
# Ensure there is a subdir that should not be reported.
os.makedirs(os.path.join(dest_dir, 'subdir'))
# And also a subdir that will be reported.
os.makedirs(os.path.join(dest_dir, 'asdf', 'report_me'))
actual = linkdirs.real_main(['linkdirs', '--delete_unexpected_files',
'--ignore_unexpected_children',
src_dir, dest_dir])
expected = [
'Refusing to delete directories: /z/y/x/asdf/report_me',
'Unexpected directory: /z/y/x/asdf/report_me',
'rmdir /z/y/x/asdf/report_me',
]
self.assertEqual(expected, actual)
# Unexpected files should be deleted.
self.assertFalse(os.path.exists('/z/y/x/the_brain'))
self.assertFalse(os.path.exists('/z/y/x/pinky'))
# But not unexpected directories.
self.assertTrue(os.path.exists('/z/y/x/asdf/report_me'))
def test_force_removes_unexp_dirs(self):
"""Delete unexpected files and directories with --force."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_dir = '/a/b/c'
self.fs.CreateFile(os.path.join(src_dir, 'file'))
# 'asdf' subdir exists here, so it will be checked in destdir.
self.fs.CreateFile(os.path.join(src_dir, 'asdf', 'file'))
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(dest_dir, 'pinky'))
self.fs.CreateFile(os.path.join(dest_dir, 'the_brain'))
# Ensure there is a subdir that should not be reported.
os.makedirs(os.path.join(dest_dir, 'subdir'))
# And also a subdir that will be removed.
os.makedirs(os.path.join(dest_dir, 'asdf', 'delete_me'))
# And create a nested subdir that will be removed. This ensures that nested
# subdirs are handled correctly, i.e. we don't delete the parent and then
# fail to delete the child.
os.makedirs(os.path.join(dest_dir, 'asdf', 'delete_me', 'delete_me_too'))
actual = linkdirs.real_main(['linkdirs', '--delete_unexpected_files',
'--ignore_unexpected_children', '--force',
src_dir, dest_dir])
self.assertEqual([], actual)
# Unexpected files should be deleted.
self.assertFalse(os.path.exists('/z/y/x/the_brain'))
self.assertFalse(os.path.exists('/z/y/x/pinky'))
# And unexpected directories.
self.assertFalse(os.path.exists('/z/y/x/asdf/delete_me'))
def test_exclusions_are_skipped(self):
"""Excluded files/dirs are skipped."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
non_skip_files = ['link_me', 'me_too']
non_skip_dirs = ['harry', 'murphy']
skip_files = ['pinky', 'the_brain']
skip_dirs = ['loki', 'molly']
skip_pattern_prefix = 'ignore-me'
skip_contents = '\n'.join(
skip_files + ['# a comment', ''] + skip_dirs
+ [os.path.join(non_skip_dirs[0], '%s*' % skip_pattern_prefix)])
skip_filename = 'skip-me'
self.fs.CreateFile(skip_filename, contents=skip_contents)
src_dir = '/a/b/c'
for dirname in skip_dirs + non_skip_dirs:
for filename in skip_files + non_skip_files:
self.fs.CreateFile(os.path.join(src_dir, dirname, filename))
for i in range(1, 5):
filename = '%s-%d' % (skip_pattern_prefix, i)
self.fs.CreateFile(os.path.join(src_dir, non_skip_dirs[0], filename))
dest_dir = '/z/y/x'
linkdirs.real_main(['linkdirs', '--ignore_file=%s' % skip_filename,
# Report unexpected files because it exercises more code
# paths.
'--report_unexpected_files',
'--ignore_unexpected_children',
src_dir, dest_dir])
files = []
for dirpath, unused_x, filenames in os.walk(dest_dir):
for filename in filenames:
files.append(os.path.join(dirpath, filename))
files.sort()
expected = ['harry/link_me', 'harry/me_too', 'murphy/link_me',
'murphy/me_too']
expected = [os.path.join(dest_dir, x) for x in expected]
self.assertEqual(expected, files)
def test_report_diffs(self):
"""Report diffs."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
src_file = '/a/b/c/file'
dest_file = '/z/y/x/file'
self.fs.CreateFile(src_file, contents='qwerty\n')
self.fs.CreateFile(dest_file, contents='asdf\n')
# Test without --force to generate diffs.
actual = linkdirs.real_main(['linkdirs', os.path.dirname(src_file),
os.path.dirname(dest_file)])
# Strip off timestamps.
actual = [re.sub(r'\t.*$', '\t', x) for x in actual]
expected = [
'--- /z/y/x/file\t',
'+++ /a/b/c/file\t',
'@@ -1 +1 @@',
'-asdf',
'+qwerty',
]
self.assertEqual(expected, actual)
# Test with --force to overwrite.
actual = linkdirs.real_main(['linkdirs', '--force',
os.path.dirname(src_file),
os.path.dirname(dest_file)])
self.assertEqual([], actual)
self.assert_files_are_linked(src_file, dest_file)
def test_argument_handling(self):
"""Bad arguments are caught."""
self.assertEqual(
['linkdirs [OPTIONS] SOURCE_DIRECTORY [...] DESTINATION_DIRECTORY'],
linkdirs.real_main(['linkdirs', '--force', '/asdf']))
self.assertEqual(
['Cannot enable --delete_unexpected_files without '
'--ignore_unexpected_children'],
linkdirs.real_main(['linkdirs', '--delete_unexpected_files',
'/asdf', '/qwerty']))
def test_force_deletes_dest(self):
"""Force deletes existing files and directories."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
filenames = ['file1', 'file2', 'file3', 'file4']
subdir = 'dir1'
src_dir = '/a/b/c'
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(src_dir, filenames[0]), contents='qwerty')
self.fs.CreateFile(os.path.join(src_dir, filenames[1]), contents='asdf')
self.fs.CreateFile(os.path.join(src_dir, filenames[2]), contents='pinky')
self.fs.CreateFile(os.path.join(src_dir, subdir, filenames[3]))
self.fs.CreateFile(os.path.join(dest_dir, filenames[0]), contents='12345')
os.makedirs(os.path.join(dest_dir, filenames[1]))
self.fs.CreateFile(os.path.join(dest_dir, filenames[2]), contents='pinky')
# Subdir in src, file in dest.
self.fs.CreateFile(os.path.join(dest_dir, subdir), contents='pinky')
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
messages = linkdirs.real_main(['linkdirs', '--force', src_dir, dest_dir])
self.assertEqual([], messages)
self.assert_files_are_linked(os.path.join(src_dir, filenames[0]),
os.path.join(dest_dir, filenames[0]))
self.assert_files_are_linked(os.path.join(src_dir, filenames[1]),
os.path.join(dest_dir, filenames[1]))
self.assertEqual('', mock_stdout.getvalue())
def test_dryrun(self):
"""Dry-run."""
# pylint: disable=no-member
# Disable "Instance of 'FakeFilesystem' has no 'CreateFile' member"
filenames = ['file1', 'file2', 'file3', 'file4', 'file5', 'file6']
subdirs = ['dir1', 'dir2', 'dir3']
src_dir = '/a/b/c'
dest_dir = '/z/y/x'
self.fs.CreateFile(os.path.join(src_dir, filenames[0]), contents='qwerty\n')
self.fs.CreateFile(os.path.join(src_dir, filenames[1]), contents='asdf')
self.fs.CreateFile(os.path.join(src_dir, filenames[2]), contents='pinky')
self.fs.CreateFile(os.path.join(src_dir, filenames[3]), contents='brain')
self.fs.CreateFile(os.path.join(src_dir, filenames[4]), contents='3 links')
# Test handling a subdir that exists.
self.fs.CreateFile(os.path.join(src_dir, subdirs[0], filenames[0]))
# Test handling a subdir that does not exist.
self.fs.CreateFile(os.path.join(src_dir, subdirs[1], filenames[0]))
# Test handling a destination that isn't a subdir.
self.fs.CreateFile(os.path.join(src_dir, subdirs[2], filenames[0]))
# Test handling of source symlinks.
os.symlink(os.path.join(src_dir, filenames[4]),
os.path.join(src_dir, filenames[5]))
self.fs.CreateFile(os.path.join(dest_dir, filenames[0]), contents='12345\n')
self.fs.CreateFile(os.path.join(dest_dir, filenames[2]), contents='pinky')
# Test handling a destination that isn't a file.
os.makedirs(os.path.join(dest_dir, filenames[3]))
# Test handling of multiply linked destination files.
self.fs.CreateFile(os.path.join(dest_dir, filenames[4]), contents='3 links')
os.link(os.path.join(dest_dir, filenames[4]),
os.path.join(dest_dir, '%s-%s' % (filenames[4], filenames[4])))
# Test handling of subdirectories.
os.makedirs(os.path.join(dest_dir, subdirs[0]))
# Test handling a destination that isn't a subdir.
self.fs.CreateFile(os.path.join(dest_dir, subdirs[2]))
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
messages = linkdirs.real_main(['linkdirs', '--dryrun', src_dir, dest_dir])
# Strip off timestamps.
messages = [re.sub(r'\t.*$', '\t', x) for x in messages]
expected = [
'--- /z/y/x/file1\t',
'+++ /a/b/c/file1\t',
'@@ -1 +1 @@',
'-12345',
'+qwerty',
'/z/y/x/dir3 is not a directory',
'/z/y/x/file4: is not a file',
'/z/y/x/file5: link count is 2',
'Skipping symbolic link /a/b/c/file6',
]
self.assertEqual(expected, messages)
self.assertFalse(os.path.samefile(os.path.join(src_dir, filenames[0]),
os.path.join(dest_dir, filenames[0])))
self.assertTrue(os.path.exists(os.path.join(dest_dir, filenames[0])))
self.assertFalse(os.path.exists(os.path.join(dest_dir, filenames[1])))
self.assertTrue(os.path.exists(os.path.join(dest_dir, filenames[2])))
self.assertFalse(os.path.isdir(os.path.join(dest_dir, subdirs[1])))
stdout = '\n'.join([
'chmod 0777 /z/y/x/dir1',
'mkdir /z/y/x/dir2',
'ln /a/b/c/file2 /z/y/x/file2',
'/a/b/c/file3 and /z/y/x/file3 are different files but have the same'
' contents; deleting and linking',
'rm /z/y/x/file3',
'ln /a/b/c/file3 /z/y/x/file3',
'ln /a/b/c/dir1/file1 /z/y/x/dir1/file1',
'ln /a/b/c/dir2/file1 /z/y/x/dir2/file1',
'ln /a/b/c/dir3/file1 /z/y/x/dir3/file1',
'',
])
self.assertMultiLineEqual(stdout, mock_stdout.getvalue())
class TestMisc(fake_filesystem_unittest.TestCase):
"""Tests for code that can't otherwise be tested."""
def setUp(self):
self.setUpPyfakefs()
@mock.patch('sys.stdout', new_callable=StringIO)
def test_safe_unlink_prints(self, mock_stdout):
"""Integration tests cannot make safe_unlink print for directories."""
test_dir = '/a/b/c'
os.makedirs(test_dir)
linkdirs.safe_unlink(test_dir, True)
self.assertEqual('rm -r %s\n' % test_dir, mock_stdout.getvalue())
if __name__ == '__main__':
unittest.main()