from __future__ import with_statement import os import pytest import shutil import tempfile import unittest from dotfiles.cli import dispatch from dotfiles.core import Dotfiles from dotfiles.utils import is_link_to def touch(fname, times=None): with open(fname, 'a'): os.utime(fname, times) class DotfilesTestCase(unittest.TestCase): def setUp(self): """Create a temporary home directory.""" self.homedir = tempfile.mkdtemp() # Create a repository for the tests to use. self.repository = os.path.join(self.homedir, 'Dotfiles') os.mkdir(self.repository) def tearDown(self): """Delete the temporary home directory and its contents.""" shutil.rmtree(self.homedir) def assertPathEqual(self, path1, path2): self.assertEqual( os.path.realpath(path1), os.path.realpath(path2)) def test_force_sync_directory(self): """Test forced sync when the dotfile is a directory. I installed the lastpass chrome extension which stores a socket in ~/.lastpass. So I added that directory as an external to /tmp and attempted a forced sync. An error occurred because sync() calls os.remove() as it mistakenly assumes the dotfile is a file and not a directory. """ os.mkdir(os.path.join(self.homedir, '.lastpass')) externals = {'.lastpass': '/tmp'} dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals=externals, packages=[], dry_run=False) dotfiles.sync(force=True) self.assertPathEqual( os.path.join(self.homedir, '.lastpass'), '/tmp') def test_dispatch(self): """Test that the force option is handed on to the sync method.""" class MockDotfiles(object): def sync(self, files=None, force=False): assert force class MockNamespace(object): def __init__(self): self.action = 'sync' self.force = True dispatch(MockDotfiles(), MockNamespace(), []) def test_move_repository(self): """Test the move() method for a Dotfiles repository.""" touch(os.path.join(self.repository, 'bashrc')) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], force=True, externals={}, packages=[], dry_run=False) dotfiles.sync() # Make sure sync() did the right thing. self.assertPathEqual( os.path.join(self.homedir, '.bashrc'), os.path.join(self.repository, 'bashrc')) target = os.path.join(self.homedir, 'MyDotfiles') dotfiles.move(target) self.assertTrue(os.path.exists(os.path.join(target, 'bashrc'))) self.assertPathEqual( os.path.join(self.homedir, '.bashrc'), os.path.join(target, 'bashrc')) def test_force_sync_directory_symlink(self): """Test a forced sync on a directory symlink. A bug was reported where a user wanted to replace a dotfile repository with an other one. They had a .vim directory in their home directory which was obviously also a symbolic link. This caused: OSError: Cannot call rmtree on a symbolic link """ # Create a dotfile symlink to some directory os.mkdir(os.path.join(self.homedir, 'vim')) os.symlink(os.path.join(self.homedir, 'vim'), os.path.join(self.homedir, '.vim')) # Create a vim directory in the repository. This will cause the above # symlink to be overwritten on sync. os.mkdir(os.path.join(self.repository, 'vim')) # Make sure the symlink points to the correct location. self.assertPathEqual( os.path.join(self.homedir, '.vim'), os.path.join(self.homedir, 'vim')) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=[], dry_run=False) dotfiles.sync(force=True) # The symlink should now point to the directory in the repository. self.assertPathEqual( os.path.join(self.homedir, '.vim'), os.path.join(self.repository, 'vim')) def test_glob_ignore_pattern(self): """ Test that the use of glob pattern matching works in the ignores list. The following repo dir exists: myscript.py myscript.pyc myscript.pyo bashrc bashrc.swp vimrc vimrc.swp install.sh Using the glob pattern dotfiles should have the following sync result in home: .myscript.py -> Dotfiles/myscript.py .bashrc -> Dotfiles/bashrc .vimrc -> Dotfiles/vimrc """ ignore = ['*.swp', '*.py?', 'install.sh'] all_repo_files = ( ('myscript.py', '.myscript.py'), ('myscript.pyc', None), ('myscript.pyo', None), ('bashrc', '.bashrc'), ('bashrc.swp', None), ('vimrc', '.vimrc'), ('vimrc.swp', None), ('install.sh', None) ) all_dotfiles = [f for f in all_repo_files if f[1] is not None] for original, symlink in all_repo_files: touch(os.path.join(self.repository, original)) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=ignore, externals={}, packages=[], dry_run=False) dotfiles.sync() # Now check that the files that should have a symlink # point to the correct file and are the only files that # exist in the home dir. self.assertEqual( sorted(os.listdir(self.homedir)), sorted([f[1] for f in all_dotfiles] + ['Dotfiles'])) for original, symlink in all_dotfiles: self.assertPathEqual( os.path.join(self.repository, original), os.path.join(self.homedir, symlink)) def test_packages(self): """ Test packages. """ files = ['foo', 'package/bar'] symlinks = ['.foo', '.package/bar'] join = os.path.join # Create files for filename in files: path = join(self.repository, filename) dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) touch(path) # Create Dotfiles object dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=['package'], dry_run=False) # Create symlinks in homedir dotfiles.sync() # Verify it created what we expect def check_all(files, symlinks): self.assertTrue(os.path.isdir(join(self.homedir, '.package'))) for src, dst in zip(files, symlinks): self.assertTrue(is_link_to(join(self.homedir, dst), join(self.repository, src))) check_all(files, symlinks) # Add files to the repository new_files = [join(self.homedir, f) for f in ['.bar', '.package/foo']] for filename in new_files: path = join(self.homedir, filename) touch(path) new_repo_files = ['bar', 'package/foo'] dotfiles.add(new_files) check_all(files + new_repo_files, symlinks + new_files) # Remove them from the repository dotfiles.remove(new_files) check_all(files, symlinks) # Move the repository self.repository = join(self.homedir, 'Dotfiles2') dotfiles.move(self.repository) check_all(files, symlinks) def test_missing_package(self): """ Test a non-existent package. """ package_file = '.package/bar' # Create Dotfiles object dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=['package'], dry_run=False) path = os.path.join(self.homedir, package_file) dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) touch(path) dotfiles.add([os.path.join(self.homedir, package_file)]) def test_single_sync(self): """ Test syncing a single file in the repo The following repo dir exists: bashrc netrc vimrc Syncing only vimrc should have the folowing sync result in home: .vimrc -> Dotfiles/vimrc """ # define the repository contents repo_files = ( ('bashrc', False), ('netrc', False), ('vimrc', True)) # populate the repository for dotfile, _ in repo_files: touch(os.path.join(self.repository, dotfile)) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=[], dry_run=False) # sync only certain dotfiles for dotfile, should_sync in repo_files: if should_sync: dotfiles.sync(files=['.%s' % dotfile]) # verify home directory contents for dotfile, should_sync in repo_files: if should_sync: self.assertPathEqual( os.path.join(self.repository, dotfile), os.path.join(self.homedir, '.%s' % dotfile)) else: self.assertFalse(os.path.exists( os.path.join(self.homedir, dotfile))) def test_missing_remove(self): """Test removing a dotfile that's been removed from the repository.""" repo_file = os.path.join(self.repository, 'testdotfile') touch(repo_file) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=[], dry_run=False) dotfiles.sync() # remove the dotfile from the repository, homedir symlink is now broken os.remove(repo_file) # remove the broken symlink dotfiles.remove(['.testdotfile']) # verify symlink was removed self.assertFalse(os.path.exists( os.path.join(self.homedir, '.testdotfile'))) def test_add_package(self): """ Test adding a package that isn't already in the repository """ package_dir = os.path.join(self.homedir, '.%s/%s' % ('config', 'gtk-3.0')) os.makedirs(package_dir) touch('%s/testfile' % package_dir) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=['config'], dry_run=False, quiet=True) # This should fail, you should not be able to add dotfiles that are # defined to be packages. dotfiles.add(['.config']) self.assertFalse(os.path.islink(os.path.join(self.homedir, '.config'))) def test_no_dot_prefix(self): # define the repository contents repo_files = ('bashrc', 'netrc', 'vimrc') # populate the repository for dotfile in repo_files: touch(os.path.join(self.repository, dotfile)) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=[], dry_run=False, no_dot_prefix=True) dotfiles.sync() # verify home directory contents for dotfile in repo_files: self.assertPathEqual( os.path.join(self.repository, dotfile), os.path.join(self.homedir, dotfile)) @pytest.mark.xfail() def test_add_package_file(self): """ Test adding a package that isn't already in the repository """ package_dir = os.path.join(self.homedir, '.%s/%s' % ('config', 'gtk-3.0')) os.makedirs(package_dir) touch('%s/testfile' % package_dir) dotfiles = Dotfiles( homedir=self.homedir, path=self.repository, prefix='', ignore=[], externals={}, packages=['config'], dry_run=False) # This should succeed and the directory structure in the repository # should be created since it didn't already exist. dotfiles.add(['.config/gtk-3.0']) self.assertTrue(os.path.islink( os.path.join(self.homedir, '.config/gtk-3.0'))) @pytest.mark.xfail() def test_package_and_prefix(self): """Test syncing a package when using a non-default prefix.""" package_dir = os.path.join(self.repository, '.config/awesome') os.makedirs(package_dir) touch('%s/testfile' % package_dir) dotfiles = Dotfiles(homedir=self.homedir, path=self.repository, prefix='.', ignore=[], externals={}, packages=['.config'], dry_run=False, quiet=True) dotfiles.sync() expected = os.path.join(self.homedir, ".config") self.assertTrue(os.path.isdir(expected)) expected = os.path.join(expected, "awesome") self.assertTrue(os.path.islink(expected)) expected = os.path.join(expected, "testfile") self.assertTrue(os.path.isfile(expected)) if __name__ == '__main__': unittest.main()