diff -Nru python-sshoot-1.4.1/CHANGES.rst python-sshoot-1.4.2/CHANGES.rst --- python-sshoot-1.4.1/CHANGES.rst 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/CHANGES.rst 2019-06-13 21:00:34.000000000 +0000 @@ -1,3 +1,10 @@ +v1.4.2 - 2019-06-13 +=================== + +- Rework tests and project setup +- Fix yaml warning (Fixes: #6) + + v1.4.1 - 2018-06-30 =================== diff -Nru python-sshoot-1.4.1/.coveragerc python-sshoot-1.4.2/.coveragerc --- python-sshoot-1.4.1/.coveragerc 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/.coveragerc 2019-06-13 21:00:34.000000000 +0000 @@ -1,5 +1,9 @@ [run] -source = - sshoot +source = sshoot omit = sshoot/main.py + +[report] +show_missing = True +fail_under = 100 +skip_covered = True diff -Nru python-sshoot-1.4.1/debian/changelog python-sshoot-1.4.2/debian/changelog --- python-sshoot-1.4.1/debian/changelog 2019-02-12 14:20:20.000000000 +0000 +++ python-sshoot-1.4.2/debian/changelog 2019-10-02 12:42:53.000000000 +0000 @@ -1,3 +1,15 @@ +python-sshoot (1.4.2-1) unstable; urgency=medium + + [ upstream ] + * New release. + + [ Jonas Smedegaard ] + * Unfuzz patch 1001. + * Declare compliance with Debian Policy 4.4.1. + * Build-depend on python3-pytest-mock. + + -- Jonas Smedegaard Wed, 02 Oct 2019 14:42:53 +0200 + python-sshoot (1.4.1-7) unstable; urgency=medium * Update watch file: Fix regular expressions. diff -Nru python-sshoot-1.4.1/debian/control python-sshoot-1.4.2/debian/control --- python-sshoot-1.4.1/debian/control 2019-02-12 14:20:02.000000000 +0000 +++ python-sshoot-1.4.2/debian/control 2019-10-02 12:41:31.000000000 +0000 @@ -12,11 +12,12 @@ python3-babel, python3-fixtures , python3-prettytable, + python3-pytest-mock, python3-setuptools, python3-xdg, python3-yaml, shellcheck , -Standards-Version: 4.3.0 +Standards-Version: 4.4.1 Vcs-Git: https://salsa.debian.org/debian/python-sshoot.git Vcs-Browser: https://salsa.debian.org/debian/python-sshoot Homepage: https://github.com/albertodonato/sshoot diff -Nru python-sshoot-1.4.1/debian/copyright_hints python-sshoot-1.4.2/debian/copyright_hints --- python-sshoot-1.4.1/debian/copyright_hints 2019-02-11 21:03:57.000000000 +0000 +++ python-sshoot-1.4.2/debian/copyright_hints 2019-10-02 12:28:54.000000000 +0000 @@ -22,13 +22,14 @@ debian/tests/control debian/tests/sshoot.t debian/watch - requirements.txt + mypy.ini setup.cfg setup.py shell-completion/sshoot sshoot/__init__.py sshoot/autocomplete.py sshoot/config.py + sshoot/conftest.py sshoot/i18n.py sshoot/listing.py sshoot/locale/it_IT/LC_MESSAGES/sshoot.mo diff -Nru python-sshoot-1.4.1/debian/patches/1001_invariable_help_output.patch python-sshoot-1.4.2/debian/patches/1001_invariable_help_output.patch --- python-sshoot-1.4.1/debian/patches/1001_invariable_help_output.patch 2018-12-22 18:23:57.000000000 +0000 +++ python-sshoot-1.4.2/debian/patches/1001_invariable_help_output.patch 2019-10-02 12:33:26.000000000 +0000 @@ -15,10 +15,10 @@ This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ --- a/sshoot/main.py +++ b/sshoot/main.py -@@ -90,7 +90,7 @@ - version='%(prog)s {}'.format(__version__)) - parser.add_argument( - '-C', '--config', default=DEFAULT_CONFIG_PATH, +@@ -97,7 +97,7 @@ + '-C', + '--config', + default=DEFAULT_CONFIG_PATH, - help=_('configuration directory (default: %(default)s)')) + help=_('configuration directory (default: $HOME/.config/sshoot)')) subparsers = parser.add_subparsers( diff -Nru python-sshoot-1.4.1/.gitignore python-sshoot-1.4.2/.gitignore --- python-sshoot-1.4.1/.gitignore 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/.gitignore 2019-06-13 21:00:34.000000000 +0000 @@ -1,9 +1,11 @@ *.pyc *.pyo /build -*.egg-info +/html +/*.egg-info +/.coverage +/.mypy_cache +/.tox .eggs .pybuild -.coverage -.tox .cache diff -Nru python-sshoot-1.4.1/.isort.cfg python-sshoot-1.4.2/.isort.cfg --- python-sshoot-1.4.1/.isort.cfg 1970-01-01 00:00:00.000000000 +0000 +++ python-sshoot-1.4.2/.isort.cfg 2019-06-13 21:00:34.000000000 +0000 @@ -0,0 +1,10 @@ +[settings] +combine_as_imports = true +force_grid_wrap = 2 +force_sort_within_sections = true +from_first = false +include_trailing_comma = true +multi_line_output = 3 +not_skip = __init__.py +order_by_type = false +use_parentheses = true diff -Nru python-sshoot-1.4.1/mypy.ini python-sshoot-1.4.2/mypy.ini --- python-sshoot-1.4.1/mypy.ini 1970-01-01 00:00:00.000000000 +0000 +++ python-sshoot-1.4.2/mypy.ini 2019-06-13 21:00:34.000000000 +0000 @@ -0,0 +1,5 @@ +[mypy] +incremental = False +warn_return_any = True +warn_unused_configs = True +ignore_missing_imports = True diff -Nru python-sshoot-1.4.1/requirements.txt python-sshoot-1.4.2/requirements.txt --- python-sshoot-1.4.1/requirements.txt 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -# Dependencies are specified in setup.py --e . --e .[testing] diff -Nru python-sshoot-1.4.1/setup.py python-sshoot-1.4.2/setup.py --- python-sshoot-1.4.1/setup.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/setup.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,23 +1,17 @@ from pathlib import Path + from setuptools import ( find_packages, setup, ) -from sshoot import ( - __doc__ as description, - __version__, -) - - -tests_require = ['fixtures'] - +tests_require = ['fixtures', 'pytest', 'pytest-mock'] config = { 'name': 'sshoot', - 'version': __version__, + 'version': '1.4.2', 'license': 'GPLv3+', - 'description': description, + 'description': 'Manage multiple sshuttle VPN sessions', 'long_description': Path('README.rst').read_text(), 'author': 'Alberto Donato', 'author_email': 'alberto.donato@gmail.com', @@ -25,25 +19,29 @@ 'maintainer_email': 'alberto.donato@gmail.com', 'url': 'https://github.com/albertodonato/sshoot', 'download_url': 'https://github.com/albertodonato/sshoot/releases', - 'packages': find_packages(), + 'packages': find_packages(include=['sshoot', 'sshoot.*']), 'include_package_data': True, - 'entry_points': {'console_scripts': ['sshoot = sshoot.main:sshoot']}, + 'entry_points': { + 'console_scripts': ['sshoot = sshoot.main:sshoot'] + }, 'test_suite': 'sshoot', 'setup_requires': ['Babel'], 'install_requires': ['PyYAML', 'prettytable', 'argcomplete', 'pyxdg'], 'tests_require': tests_require, - 'extras_require': {'testing': tests_require}, + 'extras_require': { + 'testing': tests_require + }, 'keywords': 'ssh sshuttle vpn', 'classifiers': [ 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', + 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', - ('License :: OSI Approved :: ' - 'GNU General Public License v3 or later (GPLv3+)'), - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: System :: Networking', - 'Topic :: Utilities']} + ( + 'License :: OSI Approved :: ' + 'GNU General Public License v3 or later (GPLv3+)'), + 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Topic :: System :: Networking', 'Topic :: Utilities' + ] +} setup(**config) diff -Nru python-sshoot-1.4.1/sshoot/autocomplete.py python-sshoot-1.4.2/sshoot/autocomplete.py --- python-sshoot-1.4.1/sshoot/autocomplete.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/autocomplete.py 2019-06-13 21:00:34.000000000 +0000 @@ -19,7 +19,7 @@ """ manager = Manager(config_path=parsed_args.config) manager.load_config() - for name in manager.get_profiles().keys(): + for name in manager.get_profiles(): if not name.startswith(prefix): continue if running is None or manager.is_running(name) == running: diff -Nru python-sshoot-1.4.1/sshoot/config.py python-sshoot-1.4.2/sshoot/config.py --- python-sshoot-1.4.1/sshoot/config.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/config.py 2019-06-13 21:00:34.000000000 +0000 @@ -53,8 +53,9 @@ def config(self): """Return a dict with the configuration.""" return { - key: value for key, value in self._config.items() - if key in self.CONFIG_KEYS} + key: value + for key, value in self._config.items() if key in self.CONFIG_KEYS + } def _reset(self): """Reset default empty config.""" @@ -66,22 +67,19 @@ if not path.exists(): return {} - return yaml.load(path.read_text()) or {} + return yaml.safe_load(path.read_text()) or {} def _build_profiles_config(self): """Return the profiles config dict to be saved to file.""" return { name: self._to_config(profile.config()) - for name, profile in self._profiles.items()} + for name, profile in self._profiles.items() + } def _from_config(self, config): """Convert a config to a params dict.""" - return { - key.replace('-', '_'): value - for key, value in config.items()} + return {key.replace('-', '_'): value for key, value in config.items()} def _to_config(self, params): """Convert a params dict to a config.""" - return { - key.replace('_', '-'): value - for key, value in params.items()} + return {key.replace('_', '-'): value for key, value in params.items()} diff -Nru python-sshoot-1.4.1/sshoot/conftest.py python-sshoot-1.4.2/sshoot/conftest.py --- python-sshoot-1.4.1/sshoot/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/conftest.py 2019-06-13 21:00:34.000000000 +0000 @@ -0,0 +1,51 @@ +from pathlib import Path + +import pytest + +from .config import Config +from .manager import Manager + + +@pytest.fixture +def config_dir(tmpdir): + """A directory for configuration files. """ + path = Path(tmpdir / 'config') + path.mkdir() + yield path + + +@pytest.fixture +def config_file(config_dir): + """The configuration file.""" + yield config_dir / 'config.yaml' + + +@pytest.fixture +def profiles_file(config_dir): + """A Path for profiles configuration file.""" + yield config_dir / 'profiles.yaml' + + +@pytest.fixture +def config(config_dir): + """A Config object configured with a temp path.""" + yield Config(config_dir) + + +@pytest.fixture +def run_dir(tmpdir): + path = Path(tmpdir / 'run') + path.mkdir() + yield path + + +@pytest.fixture +def sessions_dir(run_dir): + path = run_dir / 'sessions' + path.mkdir() + yield path + + +@pytest.fixture +def profile_manager(config_dir, run_dir): + yield Manager(config_path=config_dir, rundir=run_dir) diff -Nru python-sshoot-1.4.1/sshoot/i18n.py python-sshoot-1.4.2/sshoot/i18n.py --- python-sshoot-1.4.1/sshoot/i18n.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/i18n.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,8 +1,8 @@ """Internationalization setup.""" -import os import argparse import gettext +import os def _setup_i18n(): diff -Nru python-sshoot-1.4.1/sshoot/__init__.py python-sshoot-1.4.2/sshoot/__init__.py --- python-sshoot-1.4.1/sshoot/__init__.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/__init__.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,4 +1,9 @@ """Manage multiple sshuttle VPN sessions.""" +from distutils.version import LooseVersion -__version__ = '1.4.1' +import pkg_resources + +__all__ = ['__version__'] + +__version__ = LooseVersion(pkg_resources.require('sshoot')[0].version) diff -Nru python-sshoot-1.4.1/sshoot/listing.py python-sshoot-1.4.2/sshoot/listing.py --- python-sshoot-1.4.1/sshoot/listing.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/listing.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,28 +1,26 @@ """Helpers for listing output.""" from collections import OrderedDict -from io import StringIO from csv import DictWriter +from io import StringIO import json from prettytable import ( + HEADER, PrettyTable, - HEADER) +) from .config import yaml_dump from .i18n import _ - # Map names to profile fileds -_FIELDS_MAP = OrderedDict([ - (_('Remote host'), 'remote'), - (_('Subnets'), 'subnets'), - (_('Auto hosts'), 'auto_hosts'), - (_('Auto nets'), 'auto_nets'), - (_('DNS forward'), 'dns'), - (_('Exclude subnets'), 'exclude_subnets'), - (_('Seed hosts'), 'seed_hosts'), - (_('Extra options'), 'extra_opts')]) +_FIELDS_MAP = OrderedDict( + [ + (_('Remote host'), 'remote'), (_('Subnets'), 'subnets'), + (_('Auto hosts'), 'auto_hosts'), (_('Auto nets'), 'auto_nets'), + (_('DNS forward'), 'dns'), (_('Exclude subnets'), 'exclude_subnets'), + (_('Seed hosts'), 'seed_hosts'), (_('Extra options'), 'extra_opts') + ]) NAME_FIELD = _('Name') STATUS_FIELD = _('Status') @@ -76,8 +74,7 @@ for name, profile in profiles_iter: row = ['*' if self.manager.is_running(name) else '', name] row.extend( - _format_value(getattr(profile, column)) - for column in columns) + _format_value(getattr(profile, column)) for column in columns) table.add_row(row) return table.get_string(sortby=NAME_FIELD) + '\n' @@ -91,11 +88,15 @@ writer.writeheader() for name, profile in profiles_iter: - row = {NAME_FIELD: name, - STATUS_FIELD: _profile_status(self.manager, name)} - row.update({ - title: getattr(profile, _FIELDS_MAP[title]) - for title in titles[2:]}) + row = { + NAME_FIELD: name, + STATUS_FIELD: _profile_status(self.manager, name) + } + row.update( + { + title: getattr(profile, _FIELDS_MAP[title]) + for title in titles[2:] + }) writer.writerow(row) return buf.getvalue() diff -Nru python-sshoot-1.4.1/sshoot/main.py python-sshoot-1.4.2/sshoot/main.py --- python-sshoot-1.4.1/sshoot/main.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/main.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,25 +1,28 @@ """Command-line interface to handle sshuttle VPN sessions.""" -import sys +from argparse import ArgumentParser +from functools import partial import os import shutil -from functools import partial -from argparse import ArgumentParser +import sys from argcomplete import autocomplete from . import __version__ +from .autocomplete import ( + complete_argument, + profile_completer, +) from .i18n import _ +from .listing import ( + profile_details, + ProfileListing, +) from .manager import ( + DEFAULT_CONFIG_PATH, Manager, ManagerProfileError, - DEFAULT_CONFIG_PATH) -from .listing import ( - ProfileListing, - profile_details) -from .autocomplete import ( - complete_argument, - profile_completer) +) class Sshoot: @@ -86,10 +89,14 @@ parser = ArgumentParser( description=_('Manage multiple sshuttle VPN sessions')) parser.add_argument( - '-V', '--version', action='version', + '-V', + '--version', + action='version', version='%(prog)s {}'.format(__version__)) parser.add_argument( - '-C', '--config', default=DEFAULT_CONFIG_PATH, + '-C', + '--config', + default=DEFAULT_CONFIG_PATH, help=_('configuration directory (default: %(default)s)')) subparsers = parser.add_subparsers( metavar='ACTION', dest='action', help=_('action to perform')) @@ -99,10 +106,11 @@ list_parser = subparsers.add_parser( 'list', help=_('list defined profiles')) list_parser.add_argument( - '-v', '--verbose', action='store_true', - help=_('verbose listing')) + '-v', '--verbose', action='store_true', help=_('verbose listing')) list_parser.add_argument( - '-f', '--format', choices=ProfileListing.supported_formats(), + '-f', + '--format', + choices=ProfileListing.supported_formats(), default='table', help=_('listing format (default %(default)s)')) @@ -122,22 +130,33 @@ create_parser.add_argument( '-r', '--remote', help=_('remote host to connect to')) create_parser.add_argument( - '-H', '--auto-hosts', action='store_true', + '-H', + '--auto-hosts', + action='store_true', help=_('automatically update /etc/hosts with hosts from VPN')) create_parser.add_argument( - '-N', '--auto-nets', action='store_true', + '-N', + '--auto-nets', + action='store_true', help=_('automatically route additional nets from server')) create_parser.add_argument( - '-d', '--dns', action='store_true', + '-d', + '--dns', + action='store_true', help=_('forward DNS queries through the VPN')) create_parser.add_argument( - '-x', '--exclude-subnets', nargs='+', + '-x', + '--exclude-subnets', + nargs='+', help=_('exclude subnets from VPN forward')) create_parser.add_argument( - '-S', '--seed-hosts', nargs='+', + '-S', + '--seed-hosts', + nargs='+', help=_('comma-separated list of hosts to seed to auto-hosts')) create_parser.add_argument( - '--extra-opts', type=str.split, + '--extra-opts', + type=str.split, help=_('extra options to pass to sshuttle command line')) # Remove profile @@ -156,7 +175,8 @@ 'name', help=_('name of the profile to start')), partial(profile_completer, running=False)) start_parser.add_argument( - 'args', nargs='*', + 'args', + nargs='*', help=('additional arguments passed to sshuttle command line.')) # Stop profile @@ -180,8 +200,7 @@ 'get-command', help=_('return the sshuttle command for a profile')) complete_argument( get_command_parser.add_argument( - 'name', help=_('name of the profile')), - profile_completer) + 'name', help=_('name of the profile')), profile_completer) # Setup autocompletion autocomplete(parser) @@ -194,14 +213,16 @@ return need_config_path_update = ( - os.path.exists(old_config_path) and - not os.path.exists(DEFAULT_CONFIG_PATH)) + os.path.exists(old_config_path) + and not os.path.exists(DEFAULT_CONFIG_PATH)) if need_config_path_update: shutil.move(old_config_path, DEFAULT_CONFIG_PATH) sys.stderr.write( - _('NOTICE: configuration tree moved from {old_path} to ' - '{new_path}\n').format( - old_path=old_config_path, new_path=DEFAULT_CONFIG_PATH)) + _( + 'NOTICE: configuration tree moved from {old_path} to ' + '{new_path}\n').format( + old_path=old_config_path, + new_path=DEFAULT_CONFIG_PATH)) def _exit(self, message=None, code=1): """Terminate with the specified error and code .""" diff -Nru python-sshoot-1.4.1/sshoot/manager.py python-sshoot-1.4.2/sshoot/manager.py --- python-sshoot-1.4.1/sshoot/manager.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/manager.py 2019-06-13 21:00:34.000000000 +0000 @@ -19,7 +19,6 @@ ProfileError, ) - DEFAULT_CONFIG_PATH = Path(xdg_config_home) / 'sshoot' @@ -30,8 +29,6 @@ class Manager: """Profile manager.""" - kill = os.kill # for testing - def __init__(self, config_path=None, rundir=None): self.config_path = ( Path(config_path) if config_path else DEFAULT_CONFIG_PATH) @@ -91,9 +88,9 @@ process = Popen(cmdline, stderr=PIPE) # Wait until process is started (it daemonizes) process.wait() - except OSError as error: + except OSError as err: # To catch file not found errors - raise ManagerProfileError(message.format(error=error)) + raise ManagerProfileError(message.format(error=str(err))) if process.returncode != 0: error = process.stderr.read().decode() @@ -110,7 +107,7 @@ try: pid = int(self._get_pidfile(name).read_text()) - self.kill(pid, SIGTERM) + os.kill(pid, SIGTERM) except (IOError, OSError) as error: raise ManagerProfileError( _('Failed to stop profile: {error}').format(error=error)) @@ -126,7 +123,7 @@ return False try: - self.kill(pid, 0) + os.kill(pid, 0) except OSError: # Delete stale pidfile pidfile.unlink() diff -Nru python-sshoot-1.4.1/sshoot/tests/test_autocomplete.py python-sshoot-1.4.2/sshoot/tests/test_autocomplete.py --- python-sshoot-1.4.1/sshoot/tests/test_autocomplete.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/tests/test_autocomplete.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,23 +1,16 @@ -from unittest import TestCase -from unittest.mock import patch -from collections import namedtuple - -from fixtures import ( - TestWithFixtures, - TempDir) +from argparse import Namespace + +import pytest -from ..manager import Manager from ..autocomplete import ( complete_argument, - profile_completer) - - -FakeParsedArgs = namedtuple('FakeParsedArgs', ['config']) + profile_completer, +) -class CompleteArgumentTests(TestCase): +class TestCompleteArgument: - def test_complete_arguments(self): + def test_complete(self): """complete_arguments attaches a completer to the argument.""" class FakeArgument: @@ -26,39 +19,37 @@ fake_argument = FakeArgument() fake_completer = object() complete_argument(fake_argument, fake_completer) - self.assertIs(fake_argument.completer, fake_completer) + assert fake_argument.completer is fake_completer -class ProfileCompleterTests(TestWithFixtures): +@pytest.fixture +def profiles(profile_manager): + yield [ + profile_manager.create_profile('foo', {'subnets': ['10.1.0.0/16']}), + profile_manager.create_profile('bar', {'subnets': ['10.2.0.0/16']}), + profile_manager.create_profile('baz', {'subnets': ['10.3.0.0/16']}) + ] - def setUp(self): - super().setUp() - self.config_path = self.useFixture(TempDir()).path - self.manager = Manager(config_path=self.config_path) - self.manager.create_profile('foo', {'subnets': ['10.1.0.0/16']}) - self.manager.create_profile('bar', {'subnets': ['10.2.0.0/16']}) - self.manager.create_profile('baz', {'subnets': ['10.2.0.0/16']}) - self.fake_args = FakeParsedArgs(self.config_path) +@pytest.fixture +def parsed_args(config_dir): + yield Namespace(config=config_dir) - def test_complete_filter_prefix(self): - """The autocomplete function returns names that match the prefix.""" - self.assertCountEqual( - ['bar', 'baz'], profile_completer('b', self.fake_args)) - @patch('sshoot.autocomplete.Manager') - def test_complete_filter_running(self, mock_manager): - """The autocomplete function returns names that match the prefix.""" - mock_manager.return_value = self.manager - self.manager.is_running = lambda name: name != 'bar' - self.assertCountEqual( - ['foo', 'baz'], - profile_completer('', self.fake_args, running=True)) +@pytest.mark.usefixtures('profiles') +class TestProfileCompleter: - @patch('sshoot.autocomplete.Manager') - def test_complete_filter_not_running(self, mock_manager): + def test_complete_filter_prefix(self, parsed_args): """The autocomplete function returns names that match the prefix.""" - mock_manager.return_value = self.manager - self.manager.is_running = lambda name: name != 'bar' - self.assertCountEqual( - ['bar'], profile_completer('', self.fake_args, running=False)) + assert list(profile_completer('b', parsed_args)) == ['bar', 'baz'] + + @pytest.mark.parametrize( + 'running,completions', [(True, ['baz', 'foo']), (False, ['bar'])]) + def test_complete_filter_running( + self, running, completions, mocker, profile_manager, parsed_args): + """The autocomplete function returns names based on running status.""" + mock_manager = mocker.patch('sshoot.autocomplete.Manager') + mock_manager.return_value = profile_manager + profile_manager.is_running = lambda name: name != 'bar' + returned = list(profile_completer('', parsed_args, running=running)) + assert returned == completions diff -Nru python-sshoot-1.4.1/sshoot/tests/test_config.py python-sshoot-1.4.2/sshoot/tests/test_config.py --- python-sshoot-1.4.1/sshoot/tests/test_config.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/tests/test_config.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,135 +1,129 @@ -from pathlib import Path from io import StringIO from textwrap import dedent -from unittest import TestCase -from fixtures import ( - TempDir, - TestWithFixtures, -) +import pytest import yaml -from ..config import ( - Config, - yaml_dump, -) +from ..config import yaml_dump from ..profile import Profile -class YamlDumpTests(TestCase): - - def setUp(self): - super().setUp() - self.data = {'foo': 'bar', 'baz': [1, 2]} +class TestYamlDump: def test_dump_to_string(self): """The method returns YAML data as a string by default.""" - result = yaml_dump(self.data) - self.assertEqual(yaml.load(stream=StringIO(result)), self.data) + data = {'foo': 'bar', 'baz': [1, 2]} + result = yaml_dump(data) + assert yaml.safe_load(stream=StringIO(result)) == data def test_dump_to_file(self): """The method dumps YAML data to the specified file.""" + data = {'foo': 'bar', 'baz': [1, 2]} fh = StringIO() - result = yaml_dump(self.data, fh=fh) + result = yaml_dump(data, fh=fh) fh.seek(0) - self.assertIsNone(result) - self.assertEqual(yaml.load(stream=fh), self.data) - + assert result is None + assert yaml.safe_load(stream=fh) == data -class ConfigTests(TestWithFixtures): - def setUp(self): - super().setUp() - self.tempdir = Path(self.useFixture(TempDir()).path) - self.config_path = self.tempdir / 'config.yaml' - self.profiles_path = self.tempdir / 'profiles.yaml' - self.config = Config(self.tempdir) +class TestConfig: - def test_add_profile(self): + def test_add_profile(self, config): """Profiles can be added to the config.""" profiles = { 'profile1': Profile(['10.0.0.0/24']), - 'profile2': Profile(['192.168.0.0/16'])} + 'profile2': Profile(['192.168.0.0/16']) + } for name, profile in profiles.items(): - self.config.add_profile(name, profile) - self.assertEqual(self.config.profiles, profiles) + config.add_profile(name, profile) + assert config.profiles == profiles - def test_add_profile_name_present(self): + def test_add_profile_name_present(self, config): """An exception is raised if the profile name is already used.""" - self.config.add_profile('profile', Profile(['10.0.0.0/24'])) - self.assertRaises( - KeyError, self.config.add_profile, 'profile', - Profile(['192.168.0.0/16'])) + config.add_profile('profile', Profile(['10.0.0.0/24'])) + with pytest.raises(KeyError): + config.add_profile('profile', Profile(['192.168.0.0/16'])) - def test_remove_profile(self): + def test_remove_profile(self, config): """Profiles can be removed to the config.""" profiles = { 'profile1': Profile(['10.0.0.0/24']), - 'profile2': Profile(['192.168.0.0/16'])} + 'profile2': Profile(['192.168.0.0/16']) + } for name, profile in profiles.items(): - self.config.add_profile(name, profile) - self.config.remove_profile('profile1') - self.assertCountEqual(self.config.profiles.keys(), ['profile2']) + config.add_profile(name, profile) + config.remove_profile('profile1') + assert list(config.profiles), ['profile2'] - def test_remove_profile_not_present(self): + def test_remove_profile_not_present(self, config): """An exception is raised if the profile name is not known.""" - self.assertRaises(KeyError, self.config.remove_profile, 'profile') + with pytest.raises(KeyError): + config.remove_profile('profile') - def test_load_from_file(self): + def test_load_from_file(self, config, profiles_file): """The config is loaded from file.""" - profiles = { - 'profile': { - 'subnets': ['10.0.0.0/24'], - 'auto-nets': True}} - self.profiles_path.write_text(yaml.dump(profiles)) - self.config.load() - profile = self.config.profiles['profile'] - self.assertEqual(profile.subnets, ['10.0.0.0/24']) - self.assertTrue(profile.auto_nets) + profiles = {'profile': {'subnets': ['10.0.0.0/24'], 'auto-nets': True}} + profiles_file.write_text(yaml.dump(profiles)) + config.load() + profile = config.profiles['profile'] + assert profile.subnets == ['10.0.0.0/24'] + assert profile.auto_nets - def test_load_missing(self): + def test_load_missing_file(self, config): """If no config files are found, config is empty.""" - self.config.load() - self.assertEqual(self.config.profiles, {}) - self.assertEqual(self.config.config, {}) + config.load() + assert config.profiles == {} + assert config.config == {} - def test_load_config_options(self): + def test_load_config_options(self, config, config_file): """Only known config options are loaded from config file.""" - config = {'executable': '/usr/bin/shuttle', 'other-conf': 'no'} - self.config_path.write_text(yaml.dump(config)) - self.config.load() - self.assertEqual( - self.config.config, {'executable': '/usr/bin/shuttle'}) + config_data = {'executable': '/usr/bin/shuttle', 'other-conf': 'no'} + config_file.write_text(yaml.dump(config_data)) + config.load() + assert config.config == {'executable': '/usr/bin/shuttle'} - def test_load_profiles(self): + def test_load_profiles(self, config, profiles_file): """The 'profiles' config field is loaded from the config file.""" profiles = { - 'profile1': {'subnets': ['10.0.0.0/24']}, - 'profile2': {'subnets': ['192.168.0.0/16']}} - self.profiles_path.write_text(yaml.dump(profiles)) - self.config.load() + 'profile1': { + 'subnets': ['10.0.0.0/24'] + }, + 'profile2': { + 'subnets': ['192.168.0.0/16'] + } + } + profiles_file.write_text(yaml.dump(profiles)) + config.load() expected = { name: Profile.from_dict(config) - for name, config in profiles.items()} - self.assertEqual(self.config.profiles, expected) + for name, config in profiles.items() + } + assert config.profiles == expected - def test_save_profiles(self): + def test_save_profiles(self, config, profiles_file): """Profiles are saved to file.""" profiles = { - 'profile1': {'subnets': ['10.0.0.0/24'], 'remote': 'hostname1'}, - 'profile2': {'subnets': ['192.168.0.0/16'], 'remote': 'hostname2'}} - self.config.load() + 'profile1': { + 'subnets': ['10.0.0.0/24'], + 'remote': 'hostname1' + }, + 'profile2': { + 'subnets': ['192.168.0.0/16'], + 'remote': 'hostname2' + } + } + config.load() for name, conf in profiles.items(): - self.config.add_profile(name, Profile.from_dict(conf)) - self.config.save() - config = yaml.load(self.profiles_path.read_text()) - self.assertEqual(config, profiles) + config.add_profile(name, Profile.from_dict(conf)) + config.save() + config = yaml.safe_load(profiles_file.read_text()) + assert config == profiles - def test_save_from_file(self): + def test_save_from_file(self, config, profiles_file): """The config is saved to file.""" conf = {'subnets': ['10.0.0.0/24'], 'auto_nets': True} - self.config.add_profile('profile', Profile.from_dict(conf)) - self.config.save() + config.add_profile('profile', Profile.from_dict(conf)) + config.save() config = dedent( '''\ @@ -138,5 +132,5 @@ subnets: - 10.0.0.0/24 ''') - content = self.profiles_path.read_text() - self.assertEqual(content, config) + content = profiles_file.read_text() + assert content == config diff -Nru python-sshoot-1.4.1/sshoot/tests/test_listing.py python-sshoot-1.4.2/sshoot/tests/test_listing.py --- python-sshoot-1.4.1/sshoot/tests/test_listing.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/tests/test_listing.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,151 +1,149 @@ import csv from io import StringIO -from pathlib import Path import json -from fixtures import ( - TempDir, - TestWithFixtures, -) +import pytest import yaml from ..listing import ( InvalidFormat, - ProfileListing, profile_details, + ProfileListing, ) -from ..manager import Manager -class ProfileListingTests(TestWithFixtures): +@pytest.fixture +def active_profiles(profile_manager): + active_profiles = [] + profile_manager.is_running = lambda name: name in active_profiles + yield active_profiles - def setUp(self): - super().setUp() - self.config_path = Path(self.useFixture(TempDir()).path) - self.rundir = Path(self.useFixture(TempDir()).path) - self.sessions_path = self.rundir / 'sessions' - self.pid_path = self.sessions_path / 'profile.pid' - self.profiles_file_path = self.config_path / 'profiles.yaml' - self.config_file_path = self.config_path / 'config.yaml' - self.sessions_path.mkdir() - - self.active_profiles = [] - self.manager = Manager( - config_path=self.config_path, rundir=self.rundir) - self.manager.sessions_path = self.sessions_path - self.manager.is_running = lambda name: name in self.active_profiles - self.profile_listing = ProfileListing(self.manager) +class TestProfileListing: def test_supported_formats(self): """supported_formats returns a list with supported formats.""" - self.assertEqual( - ProfileListing.supported_formats(), - ['csv', 'json', 'table', 'yaml']) + assert ProfileListing.supported_formats() == [ + 'csv', 'json', 'table', 'yaml' + ] - def test_get_output_unsupported_format(self): + def test_get_output_unsupported_format(self, profile_manager): """get_output raises an error if an unsupported format is passed.""" - self.assertRaises( - InvalidFormat, self.profile_listing.get_output, 'unknown') + with pytest.raises(InvalidFormat): + ProfileListing(profile_manager).get_output('unknown') - def test_get_output_table(self): + def test_get_output_table(self, profile_manager, active_profiles): """Profiles can be listed as a table.""" - self.manager.create_profile('profile1', {'subnets': ['10.0.0.0/24']}) - self.manager.create_profile( + profile_manager.create_profile( + 'profile1', {'subnets': ['10.0.0.0/24']}) + profile_manager.create_profile( 'profile2', {'subnets': ['192.168.0.0/16']}) - self.active_profiles.append('profile2') - output = self.profile_listing.get_output('table') - self.assertIn(' profile1 10.0.0.0/24', output) - self.assertIn('* profile2 192.168.0.0/16', output) + active_profiles.append('profile2') + output = ProfileListing(profile_manager).get_output('table') + assert ' profile1 10.0.0.0/24' in output + assert '* profile2 192.168.0.0/16' in output - def test_get_output_table_verbose(self): + def test_get_output_table_verbose(self, profile_manager, active_profiles): """Tabular output can be verbose.""" - self.manager.create_profile( - 'profile1', {'subnets': ['10.0.0.0/24'], 'auto_hosts': True}) - self.active_profiles.append('profile2') - output = self.profile_listing.get_output('table', verbose=True) - self.assertIn( + profile_manager.create_profile( + 'profile1', { + 'subnets': ['10.0.0.0/24'], + 'auto_hosts': True + }) + active_profiles.append('profile2') + output = ProfileListing(profile_manager).get_output( + 'table', verbose=True) + assert ( 'Name Remote host Subnets Auto hosts Auto nets' - ' DNS forward Exclude subnets Seed hosts Extra options', - output) - self.assertIn( - 'profile1 10.0.0.0/24 True False False', + ' DNS forward Exclude subnets Seed hosts Extra options' in output) + assert ( + 'profile1 10.0.0.0/24 True False False' + in output) - def test_get_output_csv(self): + def test_get_output_csv(self, profile_manager, active_profiles): """Profiles can be listed as CSV.""" - self.manager.create_profile('profile1', {'subnets': ['10.0.0.0/24']}) - self.manager.create_profile( + profile_manager.create_profile( + 'profile1', {'subnets': ['10.0.0.0/24']}) + profile_manager.create_profile( 'profile2', {'subnets': ['192.168.0.0/16']}) - self.active_profiles.append('profile2') - output = self.profile_listing.get_output('csv') + active_profiles.append('profile2') + output = ProfileListing(profile_manager).get_output('csv') reader = csv.reader(StringIO(output)) - self.assertEqual( - sorted(reader), - [['Name', 'Status', 'Remote host', 'Subnets', 'Auto hosts', - 'Auto nets', 'DNS forward', 'Exclude subnets', 'Seed hosts', - 'Extra options'], - ['profile1', 'STOPPED', '', "['10.0.0.0/24']", 'False', 'False', - 'False', '', '', ''], - ['profile2', 'ACTIVE', '', "['192.168.0.0/16']", 'False', 'False', - 'False', '', '', '']]) + assert sorted(reader) == [ + [ + 'Name', 'Status', 'Remote host', 'Subnets', 'Auto hosts', + 'Auto nets', 'DNS forward', 'Exclude subnets', 'Seed hosts', + 'Extra options' + ], + [ + 'profile1', 'STOPPED', '', "['10.0.0.0/24']", 'False', 'False', + 'False', '', '', '' + ], + [ + 'profile2', 'ACTIVE', '', "['192.168.0.0/16']", 'False', + 'False', 'False', '', '', '' + ] + ] - def test_get_output_json(self): + def test_get_output_json(self, profile_manager, active_profiles): """Profiles can be listed as JSON.""" - self.manager.create_profile('profile1', {'subnets': ['10.0.0.0/24']}) - self.manager.create_profile( - 'profile2', {'subnets': ['192.168.0.0/16'], 'auto_hosts': True}) - self.active_profiles.append('profile2') - output = self.profile_listing.get_output('json') + profile_manager.create_profile( + 'profile1', {'subnets': ['10.0.0.0/24']}) + profile_manager.create_profile( + 'profile2', { + 'subnets': ['192.168.0.0/16'], + 'auto_hosts': True + }) + active_profiles.append('profile2') + output = ProfileListing(profile_manager).get_output('json') data = json.loads(output) - self.assertEqual( - data, - {'profile1': {'subnets': ['10.0.0.0/24']}, - 'profile2': {'subnets': ['192.168.0.0/16'], 'auto_hosts': True}}) + assert data == { + 'profile1': { + 'subnets': ['10.0.0.0/24'] + }, + 'profile2': { + 'subnets': ['192.168.0.0/16'], + 'auto_hosts': True + } + } - def test_get_output_yaml(self): + def test_get_output_yaml(self, profile_manager, active_profiles): """Profiles can be listed as YAML.""" - self.manager.create_profile('profile1', {'subnets': ['10.0.0.0/24']}) - self.manager.create_profile( - 'profile2', {'subnets': ['192.168.0.0/16'], 'auto_hosts': True}) - self.active_profiles.append('profile2') - output = self.profile_listing.get_output('yaml') - data = yaml.load(output) - self.assertEqual( - data, - {'profile1': {'subnets': ['10.0.0.0/24']}, - 'profile2': {'subnets': ['192.168.0.0/16'], 'auto_hosts': True}}) - - -class ProfileDetailsTests(TestWithFixtures): - - def setUp(self): - super().setUp() - self.config_path = Path(self.useFixture(TempDir()).path) - self.rundir = Path(self.useFixture(TempDir()).path) - self.sessions_path = self.rundir / 'sessions' - self.pid_path = self.sessions_path / 'profile.pid' - self.profiles_file_path = self.config_path / 'profiles.yaml' - self.config_file_path = self.config_path / 'config.yaml' - self.sessions_path.mkdir() - - self.active_profiles = [] - self.manager = Manager( - config_path=self.config_path, rundir=self.rundir) - self.manager.sessions_path = self.sessions_path - self.manager.is_running = lambda name: name in self.active_profiles + profile_manager.create_profile( + 'profile1', {'subnets': ['10.0.0.0/24']}) + profile_manager.create_profile( + 'profile2', { + 'subnets': ['192.168.0.0/16'], + 'auto_hosts': True + }) + active_profiles.append('profile2') + output = ProfileListing(profile_manager).get_output('yaml') + data = yaml.safe_load(output) + assert data == { + 'profile1': { + 'subnets': ['10.0.0.0/24'] + }, + 'profile2': { + 'subnets': ['192.168.0.0/16'], + 'auto_hosts': True + } + } + + +class TestProfileDetails: - def test_details(self): + def test_details(self, profile_manager): """profile_details returns a string with profile details.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - output = profile_details(self.manager, 'profile') - self.assertIn('Name: profile', output) - self.assertIn('Subnets: 10.0.0.0/24', output) - self.assertIn('Status: STOPPED', output) + profile_manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) + output = profile_details(profile_manager, 'profile') + assert 'Name: profile' in output + assert 'Subnets: 10.0.0.0/24' in output + assert 'Status: STOPPED' in output - def test_active(self): + def test_active(self, profile_manager, active_profiles): """profile_details shows if the profile is active.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.active_profiles.append('profile') - output = profile_details(self.manager, 'profile') - self.assertIn('Status: ACTIVE', output) + profile_manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) + active_profiles.append('profile') + output = profile_details(profile_manager, 'profile') + assert 'Status: ACTIVE' in output diff -Nru python-sshoot-1.4.1/sshoot/tests/test_manager.py python-sshoot-1.4.2/sshoot/tests/test_manager.py --- python-sshoot-1.4.1/sshoot/tests/test_manager.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/tests/test_manager.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,290 +1,271 @@ -import os from getpass import getuser +import os from pathlib import Path from tempfile import gettempdir -from unittest import TestCase -from fixtures import ( - TempDir, - TestWithFixtures, -) +import pytest import yaml -from ..profile import Profile from ..manager import ( DEFAULT_CONFIG_PATH, get_rundir, Manager, ManagerProfileError, ) +from ..profile import Profile -class ManagerTests(TestWithFixtures): - - def setUp(self): - super().setUp() - self.config_path = Path(self.useFixture(TempDir()).path) - self.rundir = Path(self.useFixture(TempDir()).path) - self.sessions_path = self.rundir / 'sessions' - self.pid_path = self.sessions_path / 'profile.pid' - self.profiles_file_path = self.config_path / 'profiles.yaml' - self.config_file_path = self.config_path / 'config.yaml' - self.sessions_path.mkdir() - self.manager = Manager( - config_path=self.config_path, rundir=self.rundir) - self.manager.sessions_path = self.sessions_path - - def make_fake_executable(self, exit_code=0): - """Create a fake executable logging command line parameters.""" - temp_dir = Path(self.useFixture(TempDir()).path) - executable = temp_dir / 'executable' - executable.write_text(( +def fake_executable(base_path, exit_code): + """Create a fake executable logging command line parameters.""" + executable = Path(base_path) / 'executable' + executable.write_text( + ( '#!/bin/sh\n' 'echo $@ > {}/cmdline\n' 'echo -n stderr message >&2\n' - 'exit {}\n').format(str(temp_dir), exit_code)) - executable.chmod(0o755) - return executable + 'exit {}\n').format(str(base_path), exit_code)) + executable.chmod(0o755) + return executable + + +@pytest.fixture +def bin_succeed(tmpdir): + yield fake_executable(tmpdir, 0) + + +@pytest.fixture +def bin_fail(tmpdir): + yield fake_executable(tmpdir, 1) + + +@pytest.fixture +def profile(profile_manager): + yield profile_manager.create_profile( + 'profile', {'subnets': ['10.0.0.0/24']}) + + +@pytest.fixture +def pid_file(profile, sessions_dir): + yield sessions_dir / 'profile.pid' + + +class TestManager: def test_default_paths(self): """A default config path is set if not specified.""" - manager = Manager() - self.assertEqual(manager.config_path, DEFAULT_CONFIG_PATH) + assert Manager().config_path == DEFAULT_CONFIG_PATH - def test_paths(self): + def test_paths(self, profile_manager, config_dir, sessions_dir): """The config and sessions are set in the Manager.""" - self.assertEqual(self.manager.config_path, self.config_path) - self.assertEqual(self.manager.sessions_path, self.sessions_path) + assert profile_manager.config_path == config_dir + assert profile_manager.sessions_path == sessions_dir - def test_load_config_create_dirs(self): + def test_load_config_create_dirs( + self, profile_manager, config_dir, sessions_dir): """Manager.load_config creates config directories.""" - self.config_path.rmdir() - self.sessions_path.rmdir() - self.manager.load_config() - self.assertTrue(self.config_path.is_dir()) - self.assertTrue(self.sessions_path.is_dir()) + config_dir.rmdir() + sessions_dir.rmdir() + profile_manager.load_config() + assert config_dir.is_dir() + assert sessions_dir.is_dir() - def test_load_profiles(self): + def test_load_profiles(self, profile_manager, profiles_file): """Manager.load_config loads the profiles.""" profiles = {'profile': {'subnets': ['10.0.0.0/16']}} - self.profiles_file_path.write_text(yaml.dump(profiles)) - self.manager.load_config() - self.assertCountEqual(self.manager.get_profiles().keys(), ['profile']) + profiles_file.write_text(yaml.dump(profiles)) + profile_manager.load_config() + assert list(profile_manager.get_profiles()) == ['profile'] - def test_create_profile(self): + def test_create_profile(self, profile_manager, profiles_file): """Manager.create_profile adds a profile with specified details.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - profiles = yaml.load(self.profiles_file_path.read_text()) - self.assertEqual(profiles, {'profile': {'subnets': ['10.0.0.0/24']}}) + profile_manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) + profiles = yaml.safe_load(profiles_file.read_text()) + assert profiles == {'profile': {'subnets': ['10.0.0.0/24']}} - def test_create_profile_in_use(self): + def test_create_profile_in_use(self, profile_manager): """Manager.create_profile raises an error if profile name is in use.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.assertRaises( - ManagerProfileError, self.manager.create_profile, - 'profile', {'subnets': ['10.0.0.0/16']}) + profile_manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) + with pytest.raises(ManagerProfileError): + profile_manager.create_profile( + 'profile', {'subnets': ['10.0.0.0/16']}) - def test_create_profile_invalid_details(self): + def test_create_profile_invalid_details(self, profile_manager): """Manager.create_profile raises an error on invalid profile info.""" - self.assertRaises( - ManagerProfileError, self.manager.create_profile, - 'profile', {'wrong': 'data'}) + with pytest.raises(ManagerProfileError): + profile_manager.create_profile('profile', {'wrong': 'data'}) - def test_remove_profile(self): + def test_remove_profile(self, profile_manager, profile, profiles_file): """Manager.remove_profile removes the specified profile.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.manager.remove_profile('profile') - config = yaml.load(self.profiles_file_path.read_text()) - self.assertEqual(config, {}) + profile_manager.remove_profile('profile') + config = yaml.safe_load(profiles_file.read_text()) + assert config == {} - def test_remove_profile_unknown(self): + def test_remove_profile_unknown(self, profile_manager): """Manager.remove_profile raises an error if name is unknown.""" - self.assertRaises( - ManagerProfileError, self.manager.remove_profile, 'unknown') + with pytest.raises(ManagerProfileError): + profile_manager.remove_profile('unknown') - def test_get_profiles(self): + def test_get_profiles(self, profile_manager): """Manager.get_profiles returns defined profiles.""" - self.manager.create_profile('profile1', {'subnets': ['10.0.0.0/24']}) - self.manager.create_profile( + profile_manager.create_profile( + 'profile1', {'subnets': ['10.0.0.0/24']}) + profile_manager.create_profile( 'profile2', {'subnets': ['192.168.0.0/16']}) - profiles = { + profile_manager.get_profiles() == { 'profile1': Profile.from_dict({'subnets': ['10.0.0.0/24']}), - 'profile2': Profile.from_dict({'subnets': ['192.168.0.0/16']})} - self.assertEqual(self.manager.get_profiles(), profiles) + 'profile2': Profile.from_dict({'subnets': ['192.168.0.0/16']}) + } - def test_get_profile(self): + def test_get_profile(self, profile_manager): """Manager.get_profile returns a profile.""" config = {'subnets': ['10.0.0.0/24']} - self.manager.create_profile('profile', config) - profile = self.manager.get_profile('profile') - self.assertEqual(profile, Profile.from_dict(config)) + profile_manager.create_profile('profile', config) + profile = profile_manager.get_profile('profile') + assert profile == Profile.from_dict(config) - def test_get_profile_unknown(self): + def test_get_profile_unknown(self, profile_manager): """Manager.get_profile raises an error if the name is unknown.""" - self.assertRaises( - ManagerProfileError, self.manager.get_profile, 'unknown') + with pytest.raises(ManagerProfileError): + profile_manager.get_profile('unknown') - def test_start_profile(self): + def test_start_profile( + self, profile_manager, profile, sessions_dir, bin_succeed): """Manager.start_profile starts a profile.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - executable = self.make_fake_executable() - self.manager._get_executable = lambda: str(executable) - - self.manager.start_profile('profile') - cmdline = (executable.parent / 'cmdline').read_text() - expected_cmdline = ( + profile_manager._get_executable = lambda: str(bin_succeed) + + profile_manager.start_profile('profile') + cmdline = (bin_succeed.parent / 'cmdline').read_text() + assert cmdline == ( '10.0.0.0/24 --daemon --pidfile {}/profile.pid\n'.format( - self.sessions_path)) - self.assertEqual(cmdline, expected_cmdline) + sessions_dir)) - def test_start_profile_extra_args(self): + def test_start_profile_extra_args( + self, profile_manager, profile, sessions_dir, bin_succeed): """Manager.start_profile can add extra arguments to command line.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - executable = self.make_fake_executable() - self.manager._get_executable = lambda: str(executable) + profile_manager._get_executable = lambda: str(bin_succeed) - self.manager.start_profile( + profile_manager.start_profile( 'profile', extra_args=['--extra1', '--extra2']) - cmdline = (executable.parent / 'cmdline').read_text() - expected_cmdline = ( + cmdline = (bin_succeed.parent / 'cmdline').read_text() + assert cmdline == ( '10.0.0.0/24 --daemon --pidfile {}/profile.pid --extra1 --extra2\n' - .format(self.sessions_path)) - self.assertEqual(cmdline, expected_cmdline) + .format(sessions_dir)) - def test_start_profile_fail(self): + def test_start_profile_fail(self, profile_manager, profile, bin_fail): """An error is raised if starting a profile fails.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - executable = self.make_fake_executable(exit_code=1) - self.manager._get_executable = lambda: str(executable) - with self.assertRaises(ManagerProfileError) as context: - self.manager.start_profile('profile') - self.assertEqual( - str(context.exception), - 'Profile failed to start: stderr message') + profile_manager._get_executable = lambda: str(bin_fail) + with pytest.raises(ManagerProfileError) as err: + profile_manager.start_profile('profile') + assert str(err.value) == 'Profile failed to start: stderr message' - def test_start_profile_executable_not_found(self): + def test_start_profile_executable_not_found( + self, profile_manager, profile): """Profile start raises an error if executable is not found.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.manager._get_executable = lambda: '/not/here' - self.assertRaises( - ManagerProfileError, self.manager.start_profile, 'profile') + profile_manager._get_executable = lambda: '/not/here' + with pytest.raises(ManagerProfileError): + profile_manager.start_profile('profile') - def test_start_profile_unknown(self): + def test_start_profile_unknown(self, profile_manager): """Trying to start an unknown profile raises an error.""" - self.assertRaises( - ManagerProfileError, self.manager.start_profile, 'unknown') + with pytest.raises(ManagerProfileError): + profile_manager.start_profile('unknown') - def test_start_profile_running(self): + def test_start_profile_running(self, profile_manager, profile): """Trying to start a running profile raises an error.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - # Fake profile as running - self.manager.is_running = lambda name: True - self.assertRaises( - ManagerProfileError, self.manager.start_profile, 'profile') + profile_manager.is_running = lambda name: True + with pytest.raises(ManagerProfileError): + profile_manager.start_profile('profile') - def test_stop_profile(self): + def test_stop_profile(self, mocker, profile_manager, pid_file): """Manager.stop_profile stops a running profile.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.pid_path.write_text('100\n') - # Mock manager calls - self.manager.is_running = lambda name: True - calls = [] - self.manager.kill = ( - lambda pid, signal: calls.append((pid, signal))) - self.manager.stop_profile('profile') - self.assertEqual(calls, [(100, 15)]) + mock_kill = mocker.patch('sshoot.manager.os.kill') + pid_file.write_text('100\n') + profile_manager.is_running = lambda name: True + profile_manager.stop_profile('profile') + mock_kill.assert_called_once_with(100, 15) - def test_stop_profile_unknown(self): + def test_stop_profile_unknown(self, profile_manager): """Trying to stop an unknown profile raises an error.""" - self.assertRaises( - ManagerProfileError, self.manager.stop_profile, 'unknown') + with pytest.raises(ManagerProfileError): + profile_manager.stop_profile('unknown') - def test_stop_profile_invalid_pidfile(self): + def test_stop_profile_invalid_pidfile(self, profile_manager, pid_file): """If pidfile contains invalid data, stopping raises an error.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.pid_path.write_text('garbage') - self.assertRaises( - ManagerProfileError, self.manager.stop_profile, 'profile') + pid_file.write_text('garbage') + with pytest.raises(ManagerProfileError): + profile_manager.stop_profile('profile') - def test_stop_profile_process_not_found(self): + def test_stop_profile_process_not_found( + self, mocker, profile_manager, pid_file): """If the process fails to stop an error is raised.""" - self.pid_path.write_text('100\n') + pid_file.write_text('100\n') - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) + mock_kill = mocker.patch('sshoot.manager.os.kill') + mock_kill.side_effect = IOError - def kill(pid, signal): - raise IOError() + profile_manager.is_running = lambda name: True + with pytest.raises(ManagerProfileError) as err: + profile_manager.stop_profile('profile') + assert 'Failed to stop profile' in str(err.value) - # Mock manager calls - self.manager.kill = kill - self.manager.is_running = lambda name: True - with self.assertRaises(ManagerProfileError) as cm: - self.manager.stop_profile('profile') - self.assertIn('Failed to stop profile', str(cm.exception)) - - def test_get_pidfile(self): + def test_get_pidfile(self, profile_manager, pid_file): """Manager._get_pidfile returns the pidfile path for a session.""" - self.assertEqual(self.manager._get_pidfile('profile'), self.pid_path) + assert profile_manager._get_pidfile('profile') == pid_file - def test_is_running(self): + def test_is_running(self, profile_manager, pid_file): """If the process is present, the profile is running.""" - self.pid_path.write_text('{}\n'.format(os.getpid())) - self.assertTrue(self.manager.is_running('profile')) + pid_file.write_text('{}\n'.format(os.getpid())) + assert profile_manager.is_running('profile') - def test_is_running_no_pidfile(self): + def test_is_running_no_pidfile(self, profile_manager): """If the pidfile is not found, the profile is not running.""" - self.assertFalse(self.manager.is_running('not-here')) + assert not profile_manager.is_running('not-here') - def test_is_running_pidfile_empty(self): + def test_is_running_pidfile_empty(self, profile_manager, pid_file): """If the pidfile is empty, the profile is not running.""" - (self.sessions_path / 'profile.pid').write_text('') - self.assertFalse(self.manager.is_running('profile')) + pid_file.write_text('') + assert not profile_manager.is_running('profile') - def test_is_running_pidfile_no_integer(self): + def test_is_running_pidfile_no_integer(self, profile_manager, pid_file): """If the pid is not an integer, the profile is not running.""" - self.pid_path.write_text('foo\n') - self.assertFalse(self.manager.is_running('profile')) + pid_file.write_text('foo\n') + assert not profile_manager.is_running('profile') - def test_is_running_pidfile_no_process(self): + def test_is_running_pidfile_no_process(self, profile_manager, pid_file): """If no process is present, the profile is not running.""" - self.pid_path.write_text('-100\n') - self.assertFalse(self.manager.is_running('profile')) + pid_file.write_text('-100\n') + assert not profile_manager.is_running('profile') # The stale pidfile is deleted. - self.assertFalse(self.pid_path.exists()) + assert not pid_file.exists() - def test_get_cmdline(self): + def test_get_cmdline(self, profile_manager, pid_file): """Manager.get_cmdline returns the command line for the profile.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - pidfile = str(self.sessions_path / 'profile.pid') - self.assertEqual( - self.manager.get_cmdline('profile'), - ['sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', pidfile]) + assert profile_manager.get_cmdline('profile') == [ + 'sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', + str(pid_file) + ] - def test_get_cmdline_extra_args(self): + def test_get_cmdline_extra_args(self, profile_manager, pid_file): """Manager.get_cmdline adds passed extra arguments to command line.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - pidfile = str(self.sessions_path / 'profile.pid') - expected_cmdline = [ - 'sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', pidfile, - '--extra1', '--extra2'] - self.assertEqual( - self.manager.get_cmdline( - 'profile', extra_args=['--extra1', '--extra2']), - expected_cmdline) + cmdline = profile_manager.get_cmdline( + 'profile', extra_args=['--extra1', '--extra2']) + assert cmdline == [ + 'sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', + str(pid_file), '--extra1', '--extra2' + ] - def test_get_cmdline_executable(self): + def test_get_cmdline_executable(self, profile_manager, pid_file): """Manager.get_cmdline uses the configured executable.""" - self.manager.create_profile('profile', {'subnets': ['10.0.0.0/24']}) - self.manager._get_executable = lambda: '/foo/sshuttle' - pidfile = str(self.sessions_path / 'profile.pid') - self.assertEqual( - self.manager.get_cmdline('profile'), - ['/foo/sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', pidfile]) + profile_manager._get_executable = lambda: '/foo/sshuttle' + assert profile_manager.get_cmdline('profile') == [ + '/foo/sshuttle', '10.0.0.0/24', '--daemon', '--pidfile', + str(pid_file) + ] -class GetRundirTests(TestCase): +class TestGetRundir: def test_rundir_path(self): """get_rundir returns a user-specific tempdir path.""" rundir_path = Path(gettempdir()) / 'foo-{}'.format(getuser()) - self.assertEqual(get_rundir('foo'), rundir_path) + assert get_rundir('foo') == rundir_path diff -Nru python-sshoot-1.4.1/sshoot/tests/test_profile.py python-sshoot-1.4.2/sshoot/tests/test_profile.py --- python-sshoot-1.4.1/sshoot/tests/test_profile.py 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/sshoot/tests/test_profile.py 2019-06-13 21:00:34.000000000 +0000 @@ -1,112 +1,112 @@ -from unittest import TestCase +import pytest from ..profile import ( Profile, - ProfileError) + ProfileError, +) -class ProfileTests(TestCase): +@pytest.fixture +def profile(): + yield Profile(['1.1.1.0/24', '10.10.0.0/16']) - def setUp(self): - super().setUp() - self.profile = Profile(['1.1.1.0/24', '10.10.0.0/16']) + +class TestProfile: def test_from_dict(self): """A Profile can be created from a dict with its attributes.""" profile = Profile.from_dict( - {'remote': '1.2.3.4', - 'subnets': ['1.1.1.0/24', '10.10.0.0/16'], - 'dns': True, - 'auto_hosts': True}) - self.assertEqual(profile.remote, '1.2.3.4') - self.assertEqual(profile.subnets, ['1.1.1.0/24', '10.10.0.0/16']) - self.assertTrue(profile.dns) - self.assertTrue(profile.auto_hosts) + { + 'remote': '1.2.3.4', + 'subnets': ['1.1.1.0/24', '10.10.0.0/16'], + 'dns': True, + 'auto_hosts': True + }) + assert profile.remote == '1.2.3.4' + assert profile.subnets == ['1.1.1.0/24', '10.10.0.0/16'] + assert profile.dns + assert profile.auto_hosts # Other attributes are set to default - self.assertFalse(profile.auto_nets) - self.assertIsNone(profile.exclude_subnets) - self.assertIsNone(profile.seed_hosts) - self.assertIsNone(profile.extra_opts) + assert not profile.auto_nets + assert profile.exclude_subnets is None + assert profile.seed_hosts is None + assert profile.extra_opts is None def test_from_dict_raise_error(self): """If the 'subnets' key is missing in config, an error is raised.""" - self.assertRaises( - ProfileError, Profile.from_dict, {'remote': '1.2.3.4'}) + with pytest.raises(ProfileError): + Profile.from_dict({'remote': '1.2.3.4'}) - def test_cmdline(self): + def test_cmdline(self, profile): """Profile.cmdline() return the sshuttle cmdline for the config.""" - self.assertEqual( - self.profile.cmdline(), ['sshuttle', '1.1.1.0/24', '10.10.0.0/16']) + assert profile.cmdline() == ['sshuttle', '1.1.1.0/24', '10.10.0.0/16'] - def test_cmdline_with_options(self): + def test_cmdline_with_options(self, profile): """Profile.cmdline() return the sshuttle cmdline for the config.""" - self.profile.remote = '1.2.3.4' - self.profile.auto_hosts = True - self.profile.auto_nets = True - self.profile.dns = True - self.assertEqual( - self.profile.cmdline(), - ['sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--remote=1.2.3.4', - '--auto-hosts', '--auto-nets', '--dns']) + profile.remote = '1.2.3.4' + profile.auto_hosts = True + profile.auto_nets = True + profile.dns = True + profile.cmdline() == [ + 'sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--remote=1.2.3.4', + '--auto-hosts', '--auto-nets', '--dns' + ] - def test_cmdline_exclude_subnets(self): + def test_cmdline_exclude_subnets(self, profile): """Profile.cmdline() includes excluded subnets in the cmdline.""" - self.profile.exclude_subnets = ['10.20.0.0/16', '10.30.0.0/16'] - self.assertEqual( - self.profile.cmdline(), - ['sshuttle', '1.1.1.0/24', '10.10.0.0/16', - '--exclude=10.20.0.0/16', '--exclude=10.30.0.0/16']) + profile.exclude_subnets = ['10.20.0.0/16', '10.30.0.0/16'] + profile.cmdline() == [ + 'sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--exclude=10.20.0.0/16', + '--exclude=10.30.0.0/16' + ] - def test_cmdline_seed_hosts(self): + def test_cmdline_seed_hosts(self, profile): """Profile.cmdline() includes seeded hosts in the cmdline.""" - self.profile.seed_hosts = ['10.1.2.3', '10.4.5.6'] - self.assertEqual( - self.profile.cmdline(), - ['sshuttle', '1.1.1.0/24', '10.10.0.0/16', - '--seed-hosts=10.1.2.3,10.4.5.6']) + profile.seed_hosts = ['10.1.2.3', '10.4.5.6'] + profile.cmdline(), [ + 'sshuttle', '1.1.1.0/24', '10.10.0.0/16', + '--seed-hosts=10.1.2.3,10.4.5.6' + ] - def test_cmdline_with_profile_extra_opts(self): + def test_cmdline_with_profile_extra_opts(self, profile): """Profile.cmdline() return the sshuttle cmdline with extra options.""" - self.profile.extra_opts = ['--verbose', '--daemon'] - self.assertEqual( - self.profile.cmdline(), - ['sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--verbose', - '--daemon']) + profile.extra_opts = ['--verbose', '--daemon'] + assert profile.cmdline() == [ + 'sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--verbose', '--daemon' + ] - def test_cmdline_with_extra_opts(self): + def test_cmdline_with_extra_opts(self, profile): """Profile.cmdline() includes extra options.""" - self.assertEqual( - self.profile.cmdline(extra_opts=['--verbose', '--daemon']), - ['sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--verbose', - '--daemon']) + profile.cmdline(extra_opts=['--verbose', '--daemon']), [ + 'sshuttle', '1.1.1.0/24', '10.10.0.0/16', '--verbose', '--daemon' + ] - def test_cmdline_with_executable(self): + def test_cmdline_with_executable(self, profile): """Profile.cmdline() uses the specified executable.""" - self.assertCountEqual( - self.profile.cmdline(executable='/bin/foo'), - ['/bin/foo', '1.1.1.0/24', '10.10.0.0/16']) + assert profile.cmdline(executable='/bin/foo') == [ + '/bin/foo', '1.1.1.0/24', '10.10.0.0/16' + ] - def test_config(self): + def test_config(self, profile): """Profile.config() returns a dict with the profile config.""" - self.profile.remote = '1.2.3.4' - self.profile.dns = True - self.assertEqual( - self.profile.config(), - {'remote': '1.2.3.4', 'dns': True, - 'subnets': ['1.1.1.0/24', '10.10.0.0/16']}) + profile.remote = '1.2.3.4' + profile.dns = True + assert profile.config() == { + 'remote': '1.2.3.4', + 'dns': True, + 'subnets': ['1.1.1.0/24', '10.10.0.0/16'] + } - def test_config_rebuild_profiles(self): + def test_config_rebuild_profiles(self, profile): """Result of Profile.config() can be used build an equal Profile.""" - profile = Profile.from_dict(self.profile.config()) - self.assertEqual(profile, self.profile) + assert Profile.from_dict(profile.config()) == profile - def test_eq(self): + def test_eq(self, profile): """Profiles can be tested for equality.""" - profile = Profile(['1.1.1.0/24', '10.10.0.0/16']) - self.assertEqual(profile, self.profile) + assert Profile(['1.1.1.0/24', '10.10.0.0/16']) == profile - def test_eq_false(self): + def test_eq_false(self, profile): """Profiles with different config don't evaluate as equal.""" - profile = Profile(['1.1.1.0/24', '10.10.0.0/16']) - profile.auto_hosts = True - self.assertNotEqual(profile, self.profile) + other_profile = Profile(['1.1.1.0/24', '10.10.0.0/16']) + other_profile.auto_hosts = True + assert other_profile != profile diff -Nru python-sshoot-1.4.1/.style.yapf python-sshoot-1.4.2/.style.yapf --- python-sshoot-1.4.1/.style.yapf 1970-01-01 00:00:00.000000000 +0000 +++ python-sshoot-1.4.2/.style.yapf 2019-06-13 21:00:34.000000000 +0000 @@ -0,0 +1,16 @@ +# -*- mode: conf -*- + +[style] +align_closing_bracket_with_visual_indent = false +allow_split_before_dict_value = false +allow_multiline_dictionary_keys = true +blank_line_before_nested_class_or_def = true +coalesce_brackets = false +column_limit = 79 +continuation_align_style = SPACE +space_between_ending_comma_and_closing_bracket = true +split_all_comma_separated_values = false +split_before_expression_after_opening_paren = true +split_before_first_argument = true +split_complex_comprehension = false +split_penalty_after_opening_bracket= 35 diff -Nru python-sshoot-1.4.1/tox.ini python-sshoot-1.4.2/tox.ini --- python-sshoot-1.4.1/tox.ini 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/tox.ini 2019-06-13 21:00:34.000000000 +0000 @@ -1,30 +1,44 @@ [tox] -envlist = py3, lint skipsdist = True +[globals] +lint_files = setup.py sshoot + [testenv] deps = - -rrequirements.txt + . + .[testing] +commands = + {envbindir}/pytest {posargs} + +[testenv:format] +deps = + isort + yapf commands = - {envpython} -m unittest discover {posargs} + {envbindir}/yapf --in-place --recursive {[globals]lint_files} + {envbindir}/isort --recursive {[globals]lint_files} [testenv:lint] deps = flake8 + isort + yapf commands = - {envbindir}/flake8 --exclude build,docs,.tox . + {envbindir}/yapf --diff --recursive {[globals]lint_files} + {envbindir}/isort --check-only --diff --recursive {[globals]lint_files} + {envbindir}/flake8 {[globals]lint_files} -[testenv:coverage] +[testenv:check] deps = - -rrequirements.txt - coverage + mypy commands = - {envbindir}/coverage run -m unittest - {envbindir}/coverage report --show-missing --fail-under=100 + {envbindir}/mypy -p sshoot {posargs} -[testenv:docs] +[testenv:coverage] deps = - -rrequirements.txt - sphinx + . + .[testing] + pytest-cov commands = - sphinx-build -b html docs html {posargs} + {envbindir}/pytest --cov {posargs} diff -Nru python-sshoot-1.4.1/.travis.yml python-sshoot-1.4.2/.travis.yml --- python-sshoot-1.4.1/.travis.yml 2018-06-30 08:29:40.000000000 +0000 +++ python-sshoot-1.4.2/.travis.yml 2019-06-13 21:00:34.000000000 +0000 @@ -1,14 +1,12 @@ language: python python: - - "3.5" - "3.6" - "3.7-dev" matrix: fast_finish: true - allow_failures: - - python: "3.7-dev" stages: - lint + - check - test install: pip install tox codecov jobs: @@ -16,5 +14,8 @@ - stage: lint script: tox -e lint python: "3.6" + - stage: check + script: tox -e check + python: "3.6" script: tox -e coverage after_success: codecov