diff --git a/packetary/api.py b/packetary/api.py index c253714..e27f1ae 100644 --- a/packetary/api.py +++ b/packetary/api.py @@ -160,18 +160,29 @@ class RepositoryApi(object): ) return stat - def get_unresolved_dependencies(self, urls): + def get_unresolved_dependencies(self, origin, main=None): """Gets list of unresolved dependencies for repository(es). - :param urls: The list of repository`s URLs + :param origin: The list of repository`s URLs + :param main: The main repository(es) URL :return: list of unresolved dependencies """ packages = PackagesTree() self.controller.load_packages( - self._get_repositories(urls), + self._get_repositories(origin), packages.add ) - return packages.get_unresolved_dependencies() + + if main is not None: + base = Index() + self.controller.load_packages( + self._get_repositories(main), + base.add + ) + else: + base = None + + return packages.get_unresolved_dependencies(base) def _get_repositories(self, urls): """Gets the set of repositories by url.""" diff --git a/packetary/cli/__init__.py b/packetary/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packetary/cli/app.py b/packetary/cli/app.py new file mode 100644 index 0000000..d55d21c --- /dev/null +++ b/packetary/cli/app.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cliff import app +from cliff.commandmanager import CommandManager + +import packetary + + +class Application(app.App): + """Main cliff application class. + + Performs initialization of the command manager and + configuration of basic engines. + """ + + def build_option_parser(self, description, version, argparse_kwargs=None): + """Specifies global options.""" + p_inst = super(Application, self) + parser = p_inst.build_option_parser(description=description, + version=version, + argparse_kwargs=argparse_kwargs) + + parser.add_argument( + "--ignore-errors-num", + type=int, + default=2, + metavar="NUMBER", + help="The number of errors that can be ignored." + ) + parser.add_argument( + "--retries-num", + type=int, + default=5, + metavar="NUMBER", + help="The number of retries." + ) + parser.add_argument( + "--threads-num", + default=3, + type=int, + metavar="NUMBER", + help="The number of threads." + ) + parser.add_argument( + "--http-proxy", + default=None, + metavar="http://username:password@proxy_host:proxy_port", + help="The URL of http proxy." + ) + parser.add_argument( + "--https-proxy", + default=None, + metavar="https://username:password@proxy_host:proxy_port", + help="The URL of https proxy." + ) + return parser + + +def main(argv=None): + return Application( + description="The utility manages packages and repositories.", + version=packetary.__version__, + command_manager=CommandManager("packetary", convert_underscores=True) + ).run(argv) + + +def debug(name, cmd_class, argv=None): + """Helper for debugging single command without package installation.""" + import sys + + if argv is None: + argv = sys.argv[1:] + + argv = [name] + argv + ["-v", "-v", "--debug"] + cmd_mgr = CommandManager("test_packetary", convert_underscores=True) + cmd_mgr.add_command(name, cmd_class) + return Application( + description="The utility manages packages and repositories.", + version="0.0.1", + command_manager=cmd_mgr + ).run(argv) diff --git a/packetary/cli/commands/__init__.py b/packetary/cli/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packetary/cli/commands/base.py b/packetary/cli/commands/base.py new file mode 100644 index 0000000..163f1cf --- /dev/null +++ b/packetary/cli/commands/base.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +from cliff import command +import six + +from packetary.cli.commands.utils import make_display_attr_getter +from packetary.cli.commands.utils import read_lines_from_file +from packetary import RepositoryApi + + +@six.add_metaclass(abc.ABCMeta) +class BaseRepoCommand(command.Command): + """Super class for packetary commands.""" + + @property + def stdout(self): + """Shortcut for self.app.stdout.""" + return self.app.stdout + + def get_parser(self, prog_name): + """Specifies common options.""" + parser = super(BaseRepoCommand, self).get_parser(prog_name) + parser.add_argument( + '-t', + '--type', + type=str, + choices=['deb', 'rpm'], + metavar='TYPE', + default='deb', + help='The type of repository.') + + parser.add_argument( + '-a', + '--arch', + type=str, + choices=["x86_64", "i386"], + metavar='ARCHITECTURE', + default="x86_64", + help='The target architecture.') + + origin_gr = parser.add_mutually_exclusive_group(required=True) + origin_gr.add_argument( + '-o', '--origin-url', + nargs="+", + dest='origins', + type=six.text_type, + metavar='URL', + help='Space separated list of URLs of origin repositories.') + + origin_gr.add_argument( + '-O', '--origin-file', + type=read_lines_from_file, + dest='origins', + metavar='FILENAME', + help='The path to file with URLs of origin repositories.') + + return parser + + def take_action(self, parsed_args): + """See the Command.take_action. + + :param parsed_args: the command-line arguments + :return: the result of take_repo_action + :rtype: object + """ + return self.take_repo_action( + RepositoryApi.create( + self.app_args, parsed_args.type, parsed_args.arch + ), + parsed_args + ) + + @abc.abstractmethod + def take_repo_action(self, api, parsed_args): + """Takes action on repository. + + :param api: the RepositoryApi instance + :param parsed_args: the command-line arguments + :return: the action result + """ + + +class BaseProduceOutputCommand(BaseRepoCommand): + columns = None + + def get_parser(self, prog_name): + parser = super(BaseProduceOutputCommand, self).get_parser(prog_name) + + group = parser.add_argument_group( + title='output formatter', + description='output formatter options', + ) + group.add_argument( + '-c', '--column', + nargs='+', + choices=self.columns, + dest='columns', + metavar='COLUMN', + default=[], + help='Space separated list of columns to include.', + ) + group.add_argument( + '-s', + '--sort-columns', + type=str, + nargs='+', + choices=self.columns, + metavar='SORT_COLUMN', + default=[self.columns[0]], + help='Space separated list of keys for sorting ' + 'the data.' + ) + group.add_argument( + '--sep', + type=six.text_type, + metavar='ROW SEPARATOR', + default=six.text_type('; '), + help='The row separator.' + ) + + return parser + + def produce_output(self, parsed_args, data): + indexes = dict( + (c, i) for i, c in enumerate(self.columns) + ) + sort_index = [indexes[c] for c in parsed_args.sort_columns] + if isinstance(data, list): + data.sort(key=lambda x: [x[i] for i in sort_index]) + else: + data = sorted(data, key=lambda x: [x[i] for i in sort_index]) + + if parsed_args.columns: + include_index = [ + indexes[c] for c in parsed_args.columns + ] + data = ((row[i] for i in include_index) for row in data) + columns = parsed_args.columns + else: + columns = self.columns + + stdout = self.stdout + sep = parsed_args.sep + + # header + stdout.write("# ") + stdout.write(sep.join(columns)) + stdout.write("\n") + + for row in data: + stdout.write(sep.join(row)) + stdout.write("\n") + + def run(self, parsed_args): + # Use custom output producer. + # cliff.lister with default formatters does not work + # with large arrays of data, because it does not support streaming + # TODO(implement custom formatter) + + formatter = make_display_attr_getter(self.columns) + data = six.moves.map(formatter, self.take_action(parsed_args)) + self.produce_output(parsed_args, data) + return 0 + + @abc.abstractmethod + def take_repo_action(self, driver, parsed_args): + """See Command.take_repo_action.""" diff --git a/packetary/cli/commands/clone.py b/packetary/cli/commands/clone.py new file mode 100644 index 0000000..5f5bc34 --- /dev/null +++ b/packetary/cli/commands/clone.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from packetary.cli.commands.base import BaseRepoCommand +from packetary.cli.commands.utils import read_lines_from_file + + +class CloneCommand(BaseRepoCommand): + """Clones the specified repository to local folder.""" + + def get_parser(self, prog_name): + parser = super(CloneCommand, self).get_parser(prog_name) + + parser.add_argument( + "-d", "--destination", + required=True, + help="The path to the destination folder." + ) + parser.add_argument( + "--clean", + dest="keep_existing", + action='store_false', + default=True, + help="Remove packages that does not exist in origin repo." + ) + + parser.add_argument( + "--sources", + action='store_true', + default=False, + help="Also copy source packages." + ) + + parser.add_argument( + "--locales", + action='store_true', + default=False, + help="Also copy localisation files." + ) + + bootstrap_group = parser.add_mutually_exclusive_group(required=False) + bootstrap_group.add_argument( + "-b", "--bootstrap", + nargs='+', + dest='bootstrap', + metavar='PACKAGE [OP VERSION]', + help="Space separated list of package relations, " + "to resolve the list of mandatory packages." + ) + bootstrap_group.add_argument( + "-B", "--bootstrap-file", + type=read_lines_from_file, + dest='bootstrap', + metavar='FILENAME', + help="Path to the file with list of package relations, " + "to resolve the list of mandatory packages." + ) + + requires_group = parser.add_mutually_exclusive_group(required=False) + requires_group.add_argument( + '-r', '--requires-url', + nargs="+", + dest='requires', + metavar='URL', + help="Space separated list of repository`s URL to calculate list " + "of dependencies, that will be used to filter packages") + + requires_group.add_argument( + '-R', '--requires-file', + type=read_lines_from_file, + dest='requires', + metavar='FILENAME', + help="The path to the file with list of repository`s URL " + "to calculate list of dependencies, " + "that will be used to filter packages") + return parser + + def take_repo_action(self, api, parsed_args): + stat = api.clone_repositories( + parsed_args.origins, + parsed_args.destination, + parsed_args.requires, + parsed_args.bootstrap, + parsed_args.keep_existing, + parsed_args.sources, + parsed_args.locales + ) + self.stdout.write( + "Packages copied: {0.copied}/{0.total}.\n".format(stat) + ) + + +def debug(argv=None): + """Helper to debug the Clone command.""" + from packetary.cli.app import debug + debug("clone", CloneCommand, argv) + + +if __name__ == "__main__": + debug() diff --git a/packetary/cli/commands/packages.py b/packetary/cli/commands/packages.py new file mode 100644 index 0000000..679d3bd --- /dev/null +++ b/packetary/cli/commands/packages.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from packetary.cli.commands.base import BaseProduceOutputCommand +from packetary.cli.commands.utils import read_lines_from_file + + +class ListOfPackages(BaseProduceOutputCommand): + """Gets the list of packages from repository(es).""" + + columns = ( + "name", + "repository", + "version", + "filename", + "filesize", + "checksum", + "obsoletes", + "provides", + "requires", + ) + + def get_parser(self, prog_name): + parser = super(ListOfPackages, self).get_parser(prog_name) + + bootstrap_group = parser.add_mutually_exclusive_group(required=False) + bootstrap_group.add_argument( + "-b", "--bootstrap", + nargs='+', + dest='bootstrap', + metavar='PACKAGE [OP VERSION]', + help="Space separated list of package relations, " + "to resolve the list of mandatory packages." + ) + bootstrap_group.add_argument( + "-B", "--bootstrap-file", + type=read_lines_from_file, + dest='bootstrap', + metavar='FILENAME', + help="Path to the file with list of package relations, " + "to resolve the list of mandatory packages." + ) + + requires_group = parser.add_mutually_exclusive_group(required=False) + requires_group.add_argument( + '-r', '--requires-url', + nargs="+", + dest='requires', + metavar='URL', + help="Space separated list of repository`s URL to calculate list " + "of dependencies, that will be used to filter packages") + + requires_group.add_argument( + '-R', '--requires-file', + type=read_lines_from_file, + dest='requires', + metavar='FILENAME', + help="The path to the file with list of repository`s URL " + "to calculate list of dependencies, " + "that will be used to filter packages") + return parser + + def take_repo_action(self, api, parsed_args): + return api.get_packages( + parsed_args.origins, + parsed_args.requires, + parsed_args.bootstrap, + ) + + +def debug(argv=None): + """Helper to debug the ListOfPackages command.""" + from packetary.cli.app import debug + debug("packages", ListOfPackages, argv) + + +if __name__ == "__main__": + debug() diff --git a/packetary/cli/commands/unresolved.py b/packetary/cli/commands/unresolved.py new file mode 100644 index 0000000..1865c57 --- /dev/null +++ b/packetary/cli/commands/unresolved.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from packetary.cli.commands.base import BaseProduceOutputCommand +from packetary.cli.commands.utils import read_lines_from_file + + +class ListOfUnresolved(BaseProduceOutputCommand): + """Gets the list of external dependencies for repository(es).""" + + columns = ( + "name", + "version", + "alternative", + ) + + def get_parser(self, prog_name): + parser = super(ListOfUnresolved, self).get_parser(prog_name) + main_group = parser.add_mutually_exclusive_group(required=False) + main_group.add_argument( + '-m', '--main-url', + nargs="+", + dest='main', + metavar='URL', + help='Space separated list of URLs of repository(es) ' + ' that are used to resolve dependencies.') + + main_group.add_argument( + '-M', '--main-file', + type=read_lines_from_file, + dest='main', + metavar='FILENAME', + help='The path to the file, that contains ' + 'list of URLs of repository(es) ' + ' that are used to resolve dependencies.') + return parser + + def take_repo_action(self, api, parsed_args): + return api.get_unresolved_dependencies( + parsed_args.origins, + parsed_args.main, + ) + + +def debug(argv=None): + """Helper to debug the ListOfUnresolved command.""" + + from packetary.cli.app import debug + debug("unresolved", ListOfUnresolved, argv) + + +if __name__ == "__main__": + debug() diff --git a/packetary/cli/commands/utils.py b/packetary/cli/commands/utils.py new file mode 100644 index 0000000..7220c0c --- /dev/null +++ b/packetary/cli/commands/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import operator + +import six + + +def read_lines_from_file(filename): + """Reads lines from file. + + Note: the line starts with '#' will be skipped. + + :param filename: the path of target file + :return: the list of lines from file + """ + with open(filename, 'r') as f: + return [ + x + for x in six.moves.map(operator.methodcaller("strip"), f) + if x and not x.startswith("#") + ] + + +def get_object_attrs(obj, attrs): + """Gets object attributes as list. + + :param obj: the target object + :param attrs: the list of attributes + :return: list of values from specified attributes. + """ + return [getattr(obj, f) for f in attrs] + + +def get_display_value(value): + """Get the displayable string for value. + + :param value: the target value + :return: the displayable string for value + """ + if value is None: + return u"-" + + if isinstance(value, list): + return u", ".join(six.text_type(x) for x in value) + return six.text_type(value) + + +def make_display_attr_getter(attrs): + """Gets formatter to convert attributes of object in displayable format. + + :param attrs: the list of attributes + :return: the formatter (callable object) + """ + return lambda x: [ + get_display_value(v) for v in get_object_attrs(x, attrs) + ] diff --git a/packetary/objects/packages_tree.py b/packetary/objects/packages_tree.py index 3ffc18f..ce1158c 100644 --- a/packetary/objects/packages_tree.py +++ b/packetary/objects/packages_tree.py @@ -37,13 +37,24 @@ class PackagesTree(Index): if package.mandatory: self.mandatory_packages.append(package) - def get_unresolved_dependencies(self, unresolved=None): + def get_unresolved_dependencies(self, base=None): """Gets the set of unresolved dependencies. - :param unresolved: the known list of unresolved packages. + :param base: the base index to resolve dependencies :return: the set of unresolved depends. """ - return self.__get_unresolved_dependencies(self) + external = self.__get_unresolved_dependencies(self) + if base is None: + return external + + unresolved = set() + for relation in external: + for rel in relation: + if base.find(rel.name, rel.version) is not None: + break + else: + unresolved.add(relation) + return unresolved def get_minimal_subset(self, main, requirements): """Gets the minimal work subset. diff --git a/packetary/tests/test_cli_commands.py b/packetary/tests/test_cli_commands.py new file mode 100644 index 0000000..5b6feff --- /dev/null +++ b/packetary/tests/test_cli_commands.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import subprocess + +# The cmd2 does not work with python3.5 +# because it tries to get access to the property mswindows, +# that was removed in 3.5 +subprocess.mswindows = False + +from packetary.cli.commands import clone +from packetary.cli.commands import packages +from packetary.cli.commands import unresolved +from packetary.tests import base +from packetary.tests.stubs.generator import gen_package +from packetary.tests.stubs.generator import gen_relation +from packetary.tests.stubs.generator import gen_repository +from packetary.tests.stubs.helpers import CallbacksAdapter + + +@mock.patch.multiple( + "packetary.api", + RepositoryController=mock.DEFAULT, + ConnectionsManager=mock.DEFAULT, + AsynchronousSection=mock.MagicMock() +) +@mock.patch( + "packetary.cli.commands.base.BaseRepoCommand.stdout" +) +class TestCliCommands(base.TestCase): + common_argv = [ + "--ignore-errors-num=3", + "--threads-num=8", + "--retries-num=10", + "--http-proxy=http://proxy", + "--https-proxy=https://proxy" + ] + + clone_argv = [ + "-o", "http://localhost/origin", + "-d", ".", + "-r", "http://localhost/requires", + "-b", "test-package", + "-t", "deb", + "-a", "x86_64", + "--clean", + ] + + packages_argv = [ + "-o", "http://localhost/origin", + "-t", "deb", + "-a", "x86_64" + ] + + unresolved_argv = [ + "-o", "http://localhost/origin", + "-t", "deb", + "-a", "x86_64" + ] + + def start_cmd(self, cmd, argv): + cmd.debug(argv + self.common_argv) + + def check_context(self, context, ConnectionsManager): + self.assertEqual(3, context._ignore_errors_num) + self.assertEqual(8, context._threads_num) + self.assertIs(context._connection, ConnectionsManager.return_value) + ConnectionsManager.assert_called_once_with( + proxy="http://proxy", + secure_proxy="https://proxy", + retries_num=10 + ) + + def test_clone_cmd(self, stdout, RepositoryController, **kwargs): + ctrl = RepositoryController.load() + ctrl.copy_packages = CallbacksAdapter() + ctrl.load_repositories = CallbacksAdapter() + ctrl.load_packages = CallbacksAdapter() + ctrl.copy_packages.return_value = [1, 0] + repo = gen_repository() + ctrl.load_repositories.side_effect = [repo, gen_repository()] + ctrl.load_packages.side_effect = [ + gen_package(repository=repo), + gen_package() + ] + self.start_cmd(clone, self.clone_argv) + RepositoryController.load.assert_called_with( + mock.ANY, "deb", "x86_64" + ) + self.check_context( + RepositoryController.load.call_args[0][0], **kwargs + ) + stdout.write.assert_called_once_with( + "Packages copied: 1/2.\n" + ) + + def test_get_packages_cmd(self, stdout, RepositoryController, **kwargs): + ctrl = RepositoryController.load() + ctrl.load_packages = CallbacksAdapter() + ctrl.load_packages.return_value = gen_package( + name="test1", + filesize=1, + requires=None, + obsoletes=None, + provides=None + ) + self.start_cmd(packages, self.packages_argv) + RepositoryController.load.assert_called_with( + mock.ANY, "deb", "x86_64" + ) + self.check_context( + RepositoryController.load.call_args[0][0], **kwargs + ) + self.assertIn( + "test1; test; 1; test1.pkg; 1;", + stdout.write.call_args_list[3][0][0] + ) + + def test_get_unresolved_cmd(self, stdout, RepositoryController, **kwargs): + ctrl = RepositoryController.load() + ctrl.load_packages = CallbacksAdapter() + ctrl.load_packages.return_value = gen_package( + name="test1", + requires=[gen_relation("test2")] + ) + self.start_cmd(unresolved, self.unresolved_argv) + RepositoryController.load.assert_called_with( + mock.ANY, "deb", "x86_64" + ) + self.check_context( + RepositoryController.load.call_args[0][0], **kwargs + ) + self.assertIn( + "test2; any; -", + stdout.write.call_args_list[3][0][0] + ) diff --git a/packetary/tests/test_command_utils.py b/packetary/tests/test_command_utils.py new file mode 100644 index 0000000..9b87ab4 --- /dev/null +++ b/packetary/tests/test_command_utils.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import mock + +from packetary.cli.commands import utils +from packetary.tests import base + + +class Dummy(object): + pass + + +class TestCommandUtils(base.TestCase): + @mock.patch("packetary.cli.commands.utils.open") + def test_read_lines_from_file(self, open_mock): + open_mock().__enter__.return_value = [ + "line1\n", + " # comment\n", + "line2 \n" + ] + + self.assertEqual( + ["line1", "line2"], + utils.read_lines_from_file("test.txt") + ) + + def test_get_object_attrs(self): + obj = Dummy() + obj.attr_int = 0 + obj.attr_str = "text" + obj.attr_none = None + self.assertEqual( + [0, "text", None], + utils.get_object_attrs(obj, ["attr_int", "attr_str", "attr_none"]) + ) + + def test_get_display_value(self): + self.assertEqual(u"", utils.get_display_value("")) + self.assertEqual(u"-", utils.get_display_value(None)) + self.assertEqual(u"0", utils.get_display_value(0)) + self.assertEqual(u"", utils.get_display_value([])) + self.assertEqual( + u"1, a, None", + utils.get_display_value([1, "a", None]) + ) + self.assertEqual(u"1", utils.get_display_value(1)) + + def test_make_display_attr_getter(self): + obj = Dummy() + obj.attr_int = 0 + obj.attr_str = "text" + obj.attr_none = None + formatter = utils.make_display_attr_getter( + ["attr_int", "attr_str", "attr_none"] + ) + self.assertEqual( + [u"0", u"text", u"-"], + formatter(obj) + ) diff --git a/packetary/tests/test_packages_tree.py b/packetary/tests/test_packages_tree.py index a1c7c87..f527ad0 100644 --- a/packetary/tests/test_packages_tree.py +++ b/packetary/tests/test_packages_tree.py @@ -46,6 +46,29 @@ class TestPackagesTree(base.TestCase): (x.name for x in unresolved) ) + def test_get_unresolved_dependencies_with_main(self): + ptree = PackagesTree() + ptree.add(generator.gen_package( + 1, requires=[generator.gen_relation("unresolved")])) + ptree.add(generator.gen_package(2, requires=None)) + ptree.add(generator.gen_package( + 3, requires=[generator.gen_relation("package1")] + )) + ptree.add(generator.gen_package( + 4, + requires=[generator.gen_relation("package5")] + )) + main = Index() + main.add(generator.gen_package(5, requires=[ + generator.gen_relation("package6") + ])) + + unresolved = ptree.get_unresolved_dependencies(main) + self.assertItemsEqual( + ["unresolved"], + (x.name for x in unresolved) + ) + def test_get_minimal_subset_with_master(self): ptree = PackagesTree() ptree.add(generator.gen_package(1, requires=None)) diff --git a/packetary/tests/test_repository_api.py b/packetary/tests/test_repository_api.py index aa9a01a..a9129c9 100644 --- a/packetary/tests/test_repository_api.py +++ b/packetary/tests/test_repository_api.py @@ -171,6 +171,29 @@ class TestRepositoryApi(base.TestCase): (x.name for x in r) ) + def test_get_unresolved_with_main(self): + controller = CallbacksAdapter() + pkg1 = generator.gen_package( + name="test1", requires=[ + generator.gen_relation("test2"), + generator.gen_relation("test3") + ] + ) + pkg2 = generator.gen_package( + name="test2", requires=[generator.gen_relation("test4")] + ) + controller.load_packages.side_effect = [ + pkg1, pkg2 + ] + api = RepositoryApi(controller) + r = api.get_unresolved_dependencies("file:///repo1", "file:///repo2") + controller.load_repositories.assert_any_call("file:///repo1") + controller.load_repositories.assert_any_call("file:///repo2") + self.assertItemsEqual( + ["test3"], + (x.name for x in r) + ) + def test_parse_requirements(self): requirements = RepositoryApi._parse_requirements( ["p1 le 2 | p2 | p3 ge 2"] diff --git a/requirements.txt b/requirements.txt index 337a4d0..a369282 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pbr>=0.8 Babel>=1.3 +cliff>=1.7.0 eventlet>=0.15 bintrees>=2.0.2 chardet>=2.0.1 diff --git a/setup.cfg b/setup.cfg index 16d9991..9a37954 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,11 @@ packetary.drivers = deb=packetary.drivers.deb_driver:DebRepositoryDriver rpm=packetary.drivers.rpm_driver:RpmRepositoryDriver +packetary = + clone=packetary.cli.commands.clone:CloneCommand + packages=packetary.cli.commands.packages:ListPackages + unresolved=packetary.cli.commands.unresolved:ListUnresolved + [build_sphinx] source-dir = doc/source build-dir = doc/build