From c1f3adc019452808ea85dbb14661979c5c277eb8 Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Fri, 15 Jan 2016 16:56:31 -0500 Subject: Move implementation and tests back to directories I think keeping the physical separation is helpful, especially as more features get added. --- conftest.py | 17 --- dotfiles.py | 365 ------------------------------------------------- dotfiles/__init__.py | 1 + dotfiles/dotfiles.py | 365 +++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 9 +- test_dotfiles.py | 164 ---------------------- tests/conftest.py | 17 +++ tests/test_dotfiles.py | 164 ++++++++++++++++++++++ 8 files changed, 551 insertions(+), 551 deletions(-) delete mode 100644 conftest.py delete mode 100644 dotfiles.py create mode 100644 dotfiles/__init__.py create mode 100644 dotfiles/dotfiles.py delete mode 100644 test_dotfiles.py create mode 100644 tests/conftest.py create mode 100644 tests/test_dotfiles.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index f28429c..0000000 --- a/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest -from click.testing import CliRunner - - -@pytest.fixture(scope='function', params=['', 'home']) -def home(request, tmpdir): - return tmpdir.ensure(request.param, dir=1) - - -@pytest.fixture(scope='function') -def repo(tmpdir): - return tmpdir.ensure('repo', dir=1) - - -@pytest.fixture(scope='function') -def runner(): - return CliRunner() diff --git a/dotfiles.py b/dotfiles.py deleted file mode 100644 index 1805e97..0000000 --- a/dotfiles.py +++ /dev/null @@ -1,365 +0,0 @@ -import py -import os -import click -import errno -from operator import attrgetter - - -DEFAULT_HOMEDIR = os.path.expanduser('~/') -DEFAULT_REPO_PATH = os.path.expanduser('~/Dotfiles') -DEFAULT_REPO_IGNORE = ['.git', '.gitignore'] - - -class DotfileException(Exception): - """An exception the CLI can handle and show to the user.""" - - def __init__(self, path, message='an unknown error occurred'): - self.message = '\'%s\' %s' % (path, message) - Exception.__init__(self, self.message) - - def __str__(self): - return 'ERROR: %s' % self.message - - -class TargetIgnored(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'targets an ignored file') - - -class IsDirectory(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is a directory') - - -class IsSymlink(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is a symlink') - - -class NotASymlink(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is not a symlink') - - -class InRepository(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is within the repository') - - -class NotRootedInHome(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'not rooted in home directory') - - -class IsNested(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is nested') - - -class NotADotfile(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'is not a dotfile') - - -class DoesNotExist(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'doest not exist') - - -class TargetExists(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'target already exists') - - -class TargetMissing(DotfileException): - - def __init__(self, path): - DotfileException.__init__(self, path, 'target is missing') - - -class Repository(object): - """A repository is a directory that contains dotfiles. - - :param repodir: the location of the repository directory - :param homedir: the location of the home directory (primarily for testing) - :param ignore: a list of targets to ignore - """ - - def __init__(self, repodir, homedir, ignore=[]): - self.ignore = ignore - self.homedir = homedir - - # create repository if needed - self.repodir = repodir.ensure(dir=1) - - def __str__(self): - """Return human-readable repository contents.""" - return ''.join('%s\n' % item for item in self.contents()).rstrip() - - def __repr__(self): - return '' % self.repodir - - def _target_to_name(self, target): - """Return the expected symlink for the given repository target.""" - return self.homedir.join('.%s' % target.basename) - - def _name_to_target(self, name): - """Return the expected repository target for the given symlink.""" - return self.repodir.join(self.homedir.bestrelpath(name)[1:]) - - def dotfile(self, name): - """Return a valid dotfile for the given path.""" - - target = self._name_to_target(name) - if target.basename in self.ignore: - raise TargetIgnored(name) - if name.check(dir=1): - raise IsDirectory(name) - - for path in name.parts(): - try: - if self.repodir.samefile(path): - raise InRepository(name) - except py.error.ENOENT: - # this occurs when the symlink does not yet exist - continue - - # if not self.homedir.samefile(name.dirname): - # raise NotRootedInHome(name) - # if name.dirname != self.homedir: - # raise IsNested(name) - if name.basename[0] != '.': - raise NotADotfile(name) - - return Dotfile(name, target) - - def dotfiles(self, path): - """Return a list of dotfiles given a path.""" - - if path.check(dir=1): - raise IsDirectory(path) - - return self.dotfile(path) - - def contents(self): - """Return a list of all dotfiles in the repository path.""" - contents = [] - for target in self.repodir.listdir(): - target = py.path.local(target) - if target.basename not in self.ignore: - contents.append(Dotfile(self._target_to_name(target), target)) - return sorted(contents, key=attrgetter('name')) - - -class Dotfile(object): - """An configuration file managed within a repository. - - :param name: name of the symlink in the home directory (~/.vimrc) - :param target: where the symlink should point to (~/Dotfiles/vimrc) - """ - - def __init__(self, name, target): - self.name = name - self.target = target - - def __str__(self): - return self.name.basename - - def __repr__(self): - return '' % self.name - - def _ensure_target_dir(self, verbose): - target_dir = py.path.local(self.target.dirname) - if not target_dir.check(): - if verbose: - click.echo('MKDIR %s' % self.target.dirname) - target_dir.ensure(dir=1) - - def _add(self, verbose): - self._ensure_target_dir(verbose) - if verbose: - click.echo('MOVE %s -> %s' % (self.name, self.target)) - self.name.move(self.target) - self._link(verbose) - - def _remove(self, verbose): - self._unlink(verbose) - if verbose: - click.echo('MOVE %s -> %s' % (self.target, self.name)) - self.target.move(self.name) - # TODO: remove directory if empty - - def _link(self, verbose): - if verbose: - click.echo('LINK %s -> %s' % (self.name, self.target)) - self.name.mksymlinkto(self.target, absolute=0) - - def _unlink(self, verbose): - if verbose: - click.echo('UNLINK %s' % self.name) - self.name.remove() - - @property - def state(self): - if self.target.check(exists=0): - # only for testing, cli should never reach this state - return 'error' - elif self.name.check(exists=0): - # no $HOME symlink - return 'missing' - elif self.name.check(link=0) or not self.name.samefile(self.target): - # if name exists but isn't a link to the target - return 'conflict' - return 'ok' - - def add(self, verbose=False): - if self.name.check(file=0): - raise DoesNotExist(self.name) - if self.name.check(dir=1): - raise IsDirectory(self.name) - if self.name.check(link=1): - raise IsSymlink(self.name) - if self.target.check(exists=1): - raise TargetExists(self.name) - self._add(verbose) - - def remove(self, verbose=False): - if not self.name.check(link=1): - raise NotASymlink(self.name) - if self.target.check(exists=0): - raise TargetMissing(self.name) - self._remove(verbose) - - # TODO: replace exceptions - - def link(self, verbose=False): - if self.name.check(exists=1): - raise OSError(errno.EEXIST, self.name) - if self.target.check(exists=0): - raise OSError(errno.ENOENT, self.target) - self._link(verbose) - - def unlink(self, verbose=False): - if self.name.check(link=0): - raise Exception('%s is not a symlink' % self.name.basename) - if self.target.check(exists=0): - raise Exception('%s does not exist' % self.target) - if not self.name.samefile(self.target): - raise Exception('good lord') - self._unlink(verbose) - - -pass_repo = click.make_pass_decorator(Repository) - - -@click.group(context_settings=dict(help_option_names=['-h', '--help'])) -@click.option('--repository', type=click.Path(), show_default=True, - default=DEFAULT_REPO_PATH) -@click.version_option() -@click.pass_context -def cli(ctx, repository): - """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy, - allowing you to keep all your dotfiles in a single directory. - """ - ctx.obj = Repository(py.path.local(repository), - py.path.local(DEFAULT_HOMEDIR), - DEFAULT_REPO_IGNORE) - - -@cli.command() -@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') -@click.argument('files', nargs=-1, type=click.Path(exists=True)) -@pass_repo -def add(repo, verbose, files): - """Replace file with symlink.""" - # TODO: repo.dotfiles() for directories - for filename in files: - try: - repo.dotfile(py.path.local(filename)).add(verbose) - click.echo('added \'%s\'' % filename) - except DotfileException as err: - click.echo(err) - - -@cli.command() -@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') -@click.argument('files', nargs=-1, type=click.Path(exists=True)) -@pass_repo -def remove(repo, verbose, files): - """Replace symlink with file.""" - for filename in files: - try: - repo.dotfile(py.path.local(filename)).remove(verbose) - click.echo('removed \'%s\'' % filename) - except DotfileException as err: - click.echo(err) - - -@cli.command() -@click.option('-v', '--verbose', is_flag=True, help='Show all dotfiles.') -@click.option('-c', '--color', is_flag=True, help='Enable color output.') -@pass_repo -def status(repo, verbose, color): - """Show all dotfiles in a non-OK state.""" - - state_info = { - 'error': {'char': 'E', 'color': None}, - 'conflict': {'char': '!', 'color': None}, - 'missing': {'char': '?', 'color': None}, - } - - if verbose: - state_info['ok'] = {'char': ' ', 'color': None} - - if color: - state_info['error']['color'] = 'red' - state_info['conflict']['color'] = 'magenta' - state_info['missing']['color'] = 'yellow' - - for dotfile in repo.contents(): - try: - char = state_info[dotfile.state]['char'] - fg = state_info[dotfile.state]['color'] - click.secho('%c %s' % (char, dotfile), fg=fg) - except KeyError: - continue - - -@cli.command() -@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') -@click.argument('files', nargs=-1, type=click.Path()) -@pass_repo -def link(repo, verbose, files): - """Create missing symlinks.""" - # TODO: no files should be interpreted as all files with confirmation - for filename in files: - try: - repo.dotfile(py.path.local(filename)).link(verbose) - click.echo('linked \'%s\'' % filename) - except DotfileException as err: - click.echo(err) - - -@cli.command() -@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') -@click.argument('files', nargs=-1, type=click.Path(exists=True)) -@pass_repo -def unlink(repo, verbose, files): - """Remove existing symlinks.""" - # TODO: no files should be interpreted as all files with confirmation - for filename in files: - try: - repo.dotfile(py.path.local(filename)).unlink(verbose) - click.echo('unlinked \'%s\'' % filename) - except DotfileException as err: - click.echo(err) diff --git a/dotfiles/__init__.py b/dotfiles/__init__.py new file mode 100644 index 0000000..8ead4c9 --- /dev/null +++ b/dotfiles/__init__.py @@ -0,0 +1 @@ +__version__ = '0.7.dev0' diff --git a/dotfiles/dotfiles.py b/dotfiles/dotfiles.py new file mode 100644 index 0000000..1805e97 --- /dev/null +++ b/dotfiles/dotfiles.py @@ -0,0 +1,365 @@ +import py +import os +import click +import errno +from operator import attrgetter + + +DEFAULT_HOMEDIR = os.path.expanduser('~/') +DEFAULT_REPO_PATH = os.path.expanduser('~/Dotfiles') +DEFAULT_REPO_IGNORE = ['.git', '.gitignore'] + + +class DotfileException(Exception): + """An exception the CLI can handle and show to the user.""" + + def __init__(self, path, message='an unknown error occurred'): + self.message = '\'%s\' %s' % (path, message) + Exception.__init__(self, self.message) + + def __str__(self): + return 'ERROR: %s' % self.message + + +class TargetIgnored(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'targets an ignored file') + + +class IsDirectory(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is a directory') + + +class IsSymlink(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is a symlink') + + +class NotASymlink(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is not a symlink') + + +class InRepository(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is within the repository') + + +class NotRootedInHome(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'not rooted in home directory') + + +class IsNested(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is nested') + + +class NotADotfile(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'is not a dotfile') + + +class DoesNotExist(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'doest not exist') + + +class TargetExists(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'target already exists') + + +class TargetMissing(DotfileException): + + def __init__(self, path): + DotfileException.__init__(self, path, 'target is missing') + + +class Repository(object): + """A repository is a directory that contains dotfiles. + + :param repodir: the location of the repository directory + :param homedir: the location of the home directory (primarily for testing) + :param ignore: a list of targets to ignore + """ + + def __init__(self, repodir, homedir, ignore=[]): + self.ignore = ignore + self.homedir = homedir + + # create repository if needed + self.repodir = repodir.ensure(dir=1) + + def __str__(self): + """Return human-readable repository contents.""" + return ''.join('%s\n' % item for item in self.contents()).rstrip() + + def __repr__(self): + return '' % self.repodir + + def _target_to_name(self, target): + """Return the expected symlink for the given repository target.""" + return self.homedir.join('.%s' % target.basename) + + def _name_to_target(self, name): + """Return the expected repository target for the given symlink.""" + return self.repodir.join(self.homedir.bestrelpath(name)[1:]) + + def dotfile(self, name): + """Return a valid dotfile for the given path.""" + + target = self._name_to_target(name) + if target.basename in self.ignore: + raise TargetIgnored(name) + if name.check(dir=1): + raise IsDirectory(name) + + for path in name.parts(): + try: + if self.repodir.samefile(path): + raise InRepository(name) + except py.error.ENOENT: + # this occurs when the symlink does not yet exist + continue + + # if not self.homedir.samefile(name.dirname): + # raise NotRootedInHome(name) + # if name.dirname != self.homedir: + # raise IsNested(name) + if name.basename[0] != '.': + raise NotADotfile(name) + + return Dotfile(name, target) + + def dotfiles(self, path): + """Return a list of dotfiles given a path.""" + + if path.check(dir=1): + raise IsDirectory(path) + + return self.dotfile(path) + + def contents(self): + """Return a list of all dotfiles in the repository path.""" + contents = [] + for target in self.repodir.listdir(): + target = py.path.local(target) + if target.basename not in self.ignore: + contents.append(Dotfile(self._target_to_name(target), target)) + return sorted(contents, key=attrgetter('name')) + + +class Dotfile(object): + """An configuration file managed within a repository. + + :param name: name of the symlink in the home directory (~/.vimrc) + :param target: where the symlink should point to (~/Dotfiles/vimrc) + """ + + def __init__(self, name, target): + self.name = name + self.target = target + + def __str__(self): + return self.name.basename + + def __repr__(self): + return '' % self.name + + def _ensure_target_dir(self, verbose): + target_dir = py.path.local(self.target.dirname) + if not target_dir.check(): + if verbose: + click.echo('MKDIR %s' % self.target.dirname) + target_dir.ensure(dir=1) + + def _add(self, verbose): + self._ensure_target_dir(verbose) + if verbose: + click.echo('MOVE %s -> %s' % (self.name, self.target)) + self.name.move(self.target) + self._link(verbose) + + def _remove(self, verbose): + self._unlink(verbose) + if verbose: + click.echo('MOVE %s -> %s' % (self.target, self.name)) + self.target.move(self.name) + # TODO: remove directory if empty + + def _link(self, verbose): + if verbose: + click.echo('LINK %s -> %s' % (self.name, self.target)) + self.name.mksymlinkto(self.target, absolute=0) + + def _unlink(self, verbose): + if verbose: + click.echo('UNLINK %s' % self.name) + self.name.remove() + + @property + def state(self): + if self.target.check(exists=0): + # only for testing, cli should never reach this state + return 'error' + elif self.name.check(exists=0): + # no $HOME symlink + return 'missing' + elif self.name.check(link=0) or not self.name.samefile(self.target): + # if name exists but isn't a link to the target + return 'conflict' + return 'ok' + + def add(self, verbose=False): + if self.name.check(file=0): + raise DoesNotExist(self.name) + if self.name.check(dir=1): + raise IsDirectory(self.name) + if self.name.check(link=1): + raise IsSymlink(self.name) + if self.target.check(exists=1): + raise TargetExists(self.name) + self._add(verbose) + + def remove(self, verbose=False): + if not self.name.check(link=1): + raise NotASymlink(self.name) + if self.target.check(exists=0): + raise TargetMissing(self.name) + self._remove(verbose) + + # TODO: replace exceptions + + def link(self, verbose=False): + if self.name.check(exists=1): + raise OSError(errno.EEXIST, self.name) + if self.target.check(exists=0): + raise OSError(errno.ENOENT, self.target) + self._link(verbose) + + def unlink(self, verbose=False): + if self.name.check(link=0): + raise Exception('%s is not a symlink' % self.name.basename) + if self.target.check(exists=0): + raise Exception('%s does not exist' % self.target) + if not self.name.samefile(self.target): + raise Exception('good lord') + self._unlink(verbose) + + +pass_repo = click.make_pass_decorator(Repository) + + +@click.group(context_settings=dict(help_option_names=['-h', '--help'])) +@click.option('--repository', type=click.Path(), show_default=True, + default=DEFAULT_REPO_PATH) +@click.version_option() +@click.pass_context +def cli(ctx, repository): + """Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy, + allowing you to keep all your dotfiles in a single directory. + """ + ctx.obj = Repository(py.path.local(repository), + py.path.local(DEFAULT_HOMEDIR), + DEFAULT_REPO_IGNORE) + + +@cli.command() +@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') +@click.argument('files', nargs=-1, type=click.Path(exists=True)) +@pass_repo +def add(repo, verbose, files): + """Replace file with symlink.""" + # TODO: repo.dotfiles() for directories + for filename in files: + try: + repo.dotfile(py.path.local(filename)).add(verbose) + click.echo('added \'%s\'' % filename) + except DotfileException as err: + click.echo(err) + + +@cli.command() +@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') +@click.argument('files', nargs=-1, type=click.Path(exists=True)) +@pass_repo +def remove(repo, verbose, files): + """Replace symlink with file.""" + for filename in files: + try: + repo.dotfile(py.path.local(filename)).remove(verbose) + click.echo('removed \'%s\'' % filename) + except DotfileException as err: + click.echo(err) + + +@cli.command() +@click.option('-v', '--verbose', is_flag=True, help='Show all dotfiles.') +@click.option('-c', '--color', is_flag=True, help='Enable color output.') +@pass_repo +def status(repo, verbose, color): + """Show all dotfiles in a non-OK state.""" + + state_info = { + 'error': {'char': 'E', 'color': None}, + 'conflict': {'char': '!', 'color': None}, + 'missing': {'char': '?', 'color': None}, + } + + if verbose: + state_info['ok'] = {'char': ' ', 'color': None} + + if color: + state_info['error']['color'] = 'red' + state_info['conflict']['color'] = 'magenta' + state_info['missing']['color'] = 'yellow' + + for dotfile in repo.contents(): + try: + char = state_info[dotfile.state]['char'] + fg = state_info[dotfile.state]['color'] + click.secho('%c %s' % (char, dotfile), fg=fg) + except KeyError: + continue + + +@cli.command() +@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') +@click.argument('files', nargs=-1, type=click.Path()) +@pass_repo +def link(repo, verbose, files): + """Create missing symlinks.""" + # TODO: no files should be interpreted as all files with confirmation + for filename in files: + try: + repo.dotfile(py.path.local(filename)).link(verbose) + click.echo('linked \'%s\'' % filename) + except DotfileException as err: + click.echo(err) + + +@cli.command() +@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.') +@click.argument('files', nargs=-1, type=click.Path(exists=True)) +@pass_repo +def unlink(repo, verbose, files): + """Remove existing symlinks.""" + # TODO: no files should be interpreted as all files with confirmation + for filename in files: + try: + repo.dotfile(py.path.local(filename)).unlink(verbose) + click.echo('unlinked \'%s\'' % filename) + except DotfileException as err: + click.echo(err) diff --git a/setup.py b/setup.py index f7f62b4..eae2667 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from setuptools import setup +from dotfiles import __version__ setup( name='dotfiles', - version='0.7.dev0', + version=__version__, author='Jon Bernard', author_email='jbernard@tuxion.com', description='Easily manage your dotfiles', @@ -12,9 +13,7 @@ setup( open('LICENSE.rst').read() + '\n\n' + open('HISTORY.rst').read()), license='ISC', - py_modules=[ - 'dotfiles' - ], + packages=['dotfiles'], setup_requires=[ 'pytest-runner', 'flake8', @@ -28,7 +27,7 @@ setup( ], entry_points={ 'console_scripts': [ - 'dotfiles=dotfiles:cli', + 'dotfiles=dotfiles.dotfiles:cli', ], }, classifiers=[ diff --git a/test_dotfiles.py b/test_dotfiles.py deleted file mode 100644 index 0fd55a6..0000000 --- a/test_dotfiles.py +++ /dev/null @@ -1,164 +0,0 @@ -import pytest - -from dotfiles import Repository, Dotfile, cli -from dotfiles import IsSymlink - - -class TestCli(object): - - def test_status(self, runner, repo, home, monkeypatch): - - def repo_init(self, *args, **kwargs): - self.ignore = [] - self.homedir = home - self.repodir = repo.ensure(dir=1) - - monkeypatch.setattr(Repository, '__init__', repo_init) - - result = runner.invoke(cli, ['status']) - assert not result.exception - assert result.output == '' - - -class TestRepository(object): - - def test_init(self, repo, home): - repo.remove() - assert repo.check(exists=0) - - r = Repository(repo, home) - assert r.repodir == repo - assert r.homedir == home - assert repo.check(exists=1, dir=1) - - def test_str(self, repo, home): - repo.ensure('a') - repo.ensure('b') - repo.ensure('c') - assert str(Repository(repo, home)) == ('.a\n' - '.b\n' - '.c') - - def test_repr(self, repo): - actual = '%r' % Repository(repo, None) - expected = '' % repo - assert actual == expected - - def test_target_to_name(self, repo, home): - actual = Repository(repo, home)._target_to_name(repo.join('foo')) - expected = home.join('.foo') - assert actual == expected - - def test_name_to_target(self, repo, home): - actual = Repository(repo, home)._name_to_target(home.join('.foo')) - expected = repo.join('foo') - assert actual == expected - - @pytest.mark.xfail(reason='TODO') - def test_dotifle(self): - assert False - - def test_contents(self, repo, home): - - assert Repository(repo, home).contents() == [] - - target_a = repo.ensure('a') - target_b = repo.ensure('b') - target_c = repo.ensure('c') - contents = Repository(repo, home).contents() - - assert contents[0].target == target_a - assert contents[1].target == target_b - assert contents[2].target == target_c - - def test_nested_name_to_target(self, repo, home): - r = Repository(repo, home) - - actual = r._name_to_target(home.join('.vim/.mrconfig')) - expected = repo.join('vim/.mrconfig') - assert actual == expected - - -class TestDotfile(object): - - def test_state_error(self, repo, home): - dotfile = Dotfile(home.join('.vimrc'), repo.join('vimrc')) - assert dotfile.state == 'error' - - def test_state_missing(self, repo, home): - dotfile = Dotfile(home.join('.vimrc'), repo.ensure('vimrc')) - assert dotfile.state == 'missing' - - def test_state_conflict(self, repo, home): - dotfile = Dotfile(home.ensure('.vimrc'), repo.ensure('vimrc')) - assert dotfile.state == 'conflict' - - def test_state_ok(self, repo, home): - name = home.join('.vimrc') - target = repo.ensure('vimrc') - - dotfile = Dotfile(name, target) - name.mksymlinkto(target) - assert dotfile.state == 'ok' - - name.remove() - assert dotfile.state == 'missing' - - @pytest.mark.parametrize('times', range(1, 4)) - def test_add(self, repo, home, times): - name = home.ensure('.vimrc') - target = repo.join('vimrc') - - Dotfile(name, target).add() - - assert target.check(file=1, link=0) - assert name.check(file=1, link=1) - assert name.samefile(target) - - for x in range(2, times): - with pytest.raises(IsSymlink): - Dotfile(name, target).add() - assert target.check(file=1, link=0) - assert name.check(file=1, link=1) - assert name.samefile(target) - - @pytest.mark.parametrize('times', range(1, 4)) - def test_remove(self, repo, home, times): - name = home.join('.vimrc') - target = repo.ensure('vimrc') - - name.mksymlinkto(target) - Dotfile(name, target).remove() - - assert not target.check() - assert name.check(file=1, link=0) - - for x in range(2, times): - with pytest.raises(Exception): - # TODO: verify exception type once those exists - Dotfile(name, target).remove() - assert not target.check() - assert name.check(file=1, link=0) - - @pytest.mark.parametrize('times', range(1, 4)) - def test_link(self, repo, home, times): - name = home.join('.vimrc') - target = repo.ensure('vimrc') - - Dotfile(name, target).link() - - assert target.check(file=1, link=0) - assert name.check(file=1, link=1) - assert name.samefile(target) - - for x in range(2, times): - with pytest.raises(Exception): - # TODO: verify exception type once those exists - Dotfile(name, target).link() - assert target.check(file=1, link=0) - assert name.check(file=1, link=1) - assert name.samefile(target) - - @pytest.mark.xfail(reason='TODO') - def test_unlink(self): - assert False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f28429c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest +from click.testing import CliRunner + + +@pytest.fixture(scope='function', params=['', 'home']) +def home(request, tmpdir): + return tmpdir.ensure(request.param, dir=1) + + +@pytest.fixture(scope='function') +def repo(tmpdir): + return tmpdir.ensure('repo', dir=1) + + +@pytest.fixture(scope='function') +def runner(): + return CliRunner() diff --git a/tests/test_dotfiles.py b/tests/test_dotfiles.py new file mode 100644 index 0000000..ffc9073 --- /dev/null +++ b/tests/test_dotfiles.py @@ -0,0 +1,164 @@ +import pytest + +from dotfiles.dotfiles import Repository, Dotfile, cli +from dotfiles.dotfiles import IsSymlink + + +class TestCli(object): + + def test_status(self, runner, repo, home, monkeypatch): + + def repo_init(self, *args, **kwargs): + self.ignore = [] + self.homedir = home + self.repodir = repo.ensure(dir=1) + + monkeypatch.setattr(Repository, '__init__', repo_init) + + result = runner.invoke(cli, ['status']) + assert not result.exception + assert result.output == '' + + +class TestRepository(object): + + def test_init(self, repo, home): + repo.remove() + assert repo.check(exists=0) + + r = Repository(repo, home) + assert r.repodir == repo + assert r.homedir == home + assert repo.check(exists=1, dir=1) + + def test_str(self, repo, home): + repo.ensure('a') + repo.ensure('b') + repo.ensure('c') + assert str(Repository(repo, home)) == ('.a\n' + '.b\n' + '.c') + + def test_repr(self, repo): + actual = '%r' % Repository(repo, None) + expected = '' % repo + assert actual == expected + + def test_target_to_name(self, repo, home): + actual = Repository(repo, home)._target_to_name(repo.join('foo')) + expected = home.join('.foo') + assert actual == expected + + def test_name_to_target(self, repo, home): + actual = Repository(repo, home)._name_to_target(home.join('.foo')) + expected = repo.join('foo') + assert actual == expected + + @pytest.mark.xfail(reason='TODO') + def test_dotifle(self): + assert False + + def test_contents(self, repo, home): + + assert Repository(repo, home).contents() == [] + + target_a = repo.ensure('a') + target_b = repo.ensure('b') + target_c = repo.ensure('c') + contents = Repository(repo, home).contents() + + assert contents[0].target == target_a + assert contents[1].target == target_b + assert contents[2].target == target_c + + def test_nested_name_to_target(self, repo, home): + r = Repository(repo, home) + + actual = r._name_to_target(home.join('.vim/.mrconfig')) + expected = repo.join('vim/.mrconfig') + assert actual == expected + + +class TestDotfile(object): + + def test_state_error(self, repo, home): + dotfile = Dotfile(home.join('.vimrc'), repo.join('vimrc')) + assert dotfile.state == 'error' + + def test_state_missing(self, repo, home): + dotfile = Dotfile(home.join('.vimrc'), repo.ensure('vimrc')) + assert dotfile.state == 'missing' + + def test_state_conflict(self, repo, home): + dotfile = Dotfile(home.ensure('.vimrc'), repo.ensure('vimrc')) + assert dotfile.state == 'conflict' + + def test_state_ok(self, repo, home): + name = home.join('.vimrc') + target = repo.ensure('vimrc') + + dotfile = Dotfile(name, target) + name.mksymlinkto(target) + assert dotfile.state == 'ok' + + name.remove() + assert dotfile.state == 'missing' + + @pytest.mark.parametrize('times', range(1, 4)) + def test_add(self, repo, home, times): + name = home.ensure('.vimrc') + target = repo.join('vimrc') + + Dotfile(name, target).add() + + assert target.check(file=1, link=0) + assert name.check(file=1, link=1) + assert name.samefile(target) + + for x in range(2, times): + with pytest.raises(IsSymlink): + Dotfile(name, target).add() + assert target.check(file=1, link=0) + assert name.check(file=1, link=1) + assert name.samefile(target) + + @pytest.mark.parametrize('times', range(1, 4)) + def test_remove(self, repo, home, times): + name = home.join('.vimrc') + target = repo.ensure('vimrc') + + name.mksymlinkto(target) + Dotfile(name, target).remove() + + assert not target.check() + assert name.check(file=1, link=0) + + for x in range(2, times): + with pytest.raises(Exception): + # TODO: verify exception type once those exists + Dotfile(name, target).remove() + assert not target.check() + assert name.check(file=1, link=0) + + @pytest.mark.parametrize('times', range(1, 4)) + def test_link(self, repo, home, times): + name = home.join('.vimrc') + target = repo.ensure('vimrc') + + Dotfile(name, target).link() + + assert target.check(file=1, link=0) + assert name.check(file=1, link=1) + assert name.samefile(target) + + for x in range(2, times): + with pytest.raises(Exception): + # TODO: verify exception type once those exists + Dotfile(name, target).link() + assert target.check(file=1, link=0) + assert name.check(file=1, link=1) + assert name.samefile(target) + + @pytest.mark.xfail(reason='TODO') + def test_unlink(self): + assert False -- cgit v1.2.3