Repository: pddg/uroboros

Issue:
Add unit test

Discussion:

Patch:
diff --git a/uroboros/command.py b/uroboros/command.py
index 59d72b5..ce21155 100644
--- a/uroboros/command.py
+++ b/uroboros/command.py
@@ -1,15 +1,16 @@
 import abc
 import argparse
 import logging
+import sys
 from typing import TYPE_CHECKING
 
 from uroboros import errors
 from uroboros import utils
+from uroboros.constants import ExitStatus
 
 if TYPE_CHECKING:
     from typing import List, Dict, Optional, Union, Set
     from uroboros.option import Option
-    from uroboros.constants import ExitStatus
 
 
 class Command(metaclass=abc.ABCMeta):
@@ -37,17 +38,21 @@ class Command(metaclass=abc.ABCMeta):
         # This is enabled after initialization.
         self._parser = None  # type: Optional[argparse.ArgumentParser]
 
-    def execute(self, args: 'Optional[argparse.Namespace]' = None) -> int:
+    def execute(self, argv: 'List[str]' = None) -> int:
         """
         Execute `uroboros.command.Command.run` internally.
         And return exit code (integer).
-        :param args:    Parsed arguments. If None is given, try to parse
+        :param argv:    Arguments to parse. If None is given, try to parse
                         `sys.argv` by the parser of this command.
         :return:        Exit status (integer only)
         """
-        if args is None:
+        try:
+            self._check_initialized()
+        except errors.CommandNotRegisteredError:
             self.initialize()
-            args = self._parser.parse_args()
+        if argv is None:
+            argv = sys.argv
+        args = self._parser.parse_args(argv)
         # Get all nested validator
         exceptions = self.validate(args)
         layer = self._layer
@@ -86,18 +91,21 @@ class Command(metaclass=abc.ABCMeta):
         :return: None
         """
         if parser is None:
-            self._parser = argparse.ArgumentParser(
-                prog=self.name,
-                description=self.description,
-                parents=[o.get_parser() for o in self.options]
-            )
-            self._parser.set_defaults(func=self.run)
+            self._parser = self.create_default_parser()
         else:
             self._parser = parser
+        # Add validator
+        validator_name = utils.get_args_validator_name(self._layer)
+        self._parser.set_defaults(**{validator_name: self.validate})
+        # Add function to execute
+        self._parser.set_defaults(func=self.run)
         self.build_option(self._parser)
+        self.initialize_sub_parsers(self._parser)
+
+    def initialize_sub_parsers(self, parser: 'argparse.ArgumentParser'):
         if len(self.sub_commands) == 0:
             return
-        parser = self._parser.add_subparsers(
+        parser = parser.add_subparsers(
             dest=utils.get_args_command_name(self._layer),
             title="Sub commands",
         )
@@ -108,13 +116,17 @@ class Command(metaclass=abc.ABCMeta):
                 help=cmd.description,
                 parents=[o.get_parser() for o in cmd.options],
             )
-            # Add validator
-            validator_name = utils.get_args_validator_name(self._layer)
-            sub_parser.set_defaults(**{validator_name: cmd.validate})
-            # Add function to execute
-            sub_parser.set_defaults(func=cmd.run)
             cmd.initialize(sub_parser)
 
+    def create_default_parser(self):
+        parser = argparse.ArgumentParser(
+            prog=self.name,
+            description=self.description,
+            parents=[o.get_parser() for o in self.options]
+        )
+        parser.set_defaults(func=self.run)
+        return parser
+
     def add_command(self, command: 'Command') -> 'Command':
         """
         Add sub command to this command.
diff --git a/uroboros/utils.py b/uroboros/utils.py
index 3616354..6aa0249 100644
--- a/uroboros/utils.py
+++ b/uroboros/utils.py
@@ -1,24 +1,6 @@
-from typing import TYPE_CHECKING
-
-from uroboros import errors
-
-if TYPE_CHECKING:
-    from typing import Dict
-    from uroboros.command import Command
-    CommandDict = Dict[Command, dict]
-    CommandsDict = Dict[Command, CommandDict]
-
-
 def get_args_command_name(layer: int):
     return "__layer{layer}_command".format(layer=layer)
 
 
 def get_args_validator_name(layer: int):
     return "__layer{layer}_validator".format(layer=layer)
-
-
-def get_matched_command(name, command_dict: 'CommandsDict') -> 'CommandDict':
-    for cmd, sub_cmds in command_dict.items():
-        if cmd.name == name:
-            return {cmd: sub_cmds}
-    raise errors.NoCommandError(name)


Test patch:
diff --git a/tests/base.py b/tests/base.py
new file mode 100644
index 0000000..124cccb
--- /dev/null
+++ b/tests/base.py
@@ -0,0 +1,73 @@
+import uroboros
+
+
+class RootCommand(uroboros.Command):
+    name = 'root'
+    description = 'command of root'
+
+    error = Exception(name)
+    value = False
+
+    def build_option(self, parser):
+        parser.add_argument(
+            '--{}'.format(self.name), action='store_true',
+            default=self.value, help='root argument'
+        )
+        return parser
+
+    def run(self, args):
+        print(args.root)
+        return 0
+
+    def validate(self, args):
+        if args.root != self.value:
+            return [self.error]
+        return []
+
+
+class SecondCommand(uroboros.Command):
+    name = 'second'
+    description = 'command of second'
+
+    error = Exception(name)
+    value = 0
+
+    def build_option(self, parser):
+        parser.add_argument(
+            '--{}'.format(self.name), type=int,
+            default=self.value, help='second argument'
+        )
+        return parser
+
+    def run(self, args):
+        print(args.second)
+        return 0
+
+    def validate(self, args):
+        if args.second != self.value:
+            return [self.error]
+        return []
+
+
+class ThirdCommand(uroboros.Command):
+    name = 'third'
+    description = 'command of third'
+
+    error = Exception(name)
+    value = 'third'
+
+    def build_option(self, parser):
+        parser.add_argument(
+            '--{}'.format(self.name), type=str,
+            default=self.value, help='third argument'
+        )
+        return parser
+
+    def run(self, args):
+        print(args.third)
+        return 0
+
+    def validate(self, args):
+        if args.third != self.value:
+            return [self.error]
+        return []
diff --git a/tests/test_command.py b/tests/test_command.py
new file mode 100644
index 0000000..5c00ec6
--- /dev/null
+++ b/tests/test_command.py
@@ -0,0 +1,219 @@
+import argparse
+import logging
+from unittest import mock
+
+import pytest
+
+from uroboros.errors import CommandDuplicateError
+from .base import RootCommand, SecondCommand, ThirdCommand
+
+
+@pytest.fixture
+def commands():
+    return RootCommand(), SecondCommand(), ThirdCommand()
+
+
+def get_sub_commands(cmd_set):
+    if len(cmd_set) == 0:
+        return []
+    cmds = []
+    for c, sub_set in cmd_set.items():
+        cmds.append(c)
+        c.sub_commands = get_sub_commands(sub_set)
+    return cmds
+
+
+class TestCommand(object):
+
+    @pytest.mark.parametrize(
+        'command', [
+            RootCommand(),
+            SecondCommand(),
+            ThirdCommand()
+        ]
+    )
+    def test_build_option(self, command):
+        parser = argparse.ArgumentParser()
+        parser = command.build_option(parser)
+        expected = command.value
+        actual = parser.get_default(command.name)
+        assert expected == actual
+
+    @pytest.mark.parametrize(
+        'command', [
+            RootCommand(),
+            SecondCommand(),
+            ThirdCommand()
+        ]
+    )
+    def test_validate(self, command):
+        valid_args = mock.MagicMock(
+            **{command.name: command.value}
+        )
+        expected = []
+        actual = command.validate(valid_args)
+        assert expected == actual
+        invalid_args = mock.MagicMock(
+            **{command.name: None}
+        )
+        expected = [command.error]
+        actual = command.validate(invalid_args)
+        assert expected == actual
+
+    @pytest.mark.parametrize(
+        'command_set', [
+            {RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+            {RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+            {RootCommand(): {}, SecondCommand(): {}, ThirdCommand(): {}}
+        ]
+    )
+    def test_increment_nest(self, command_set):
+        result_set = {}
+
+        def count_nest(cmd_set, count):
+            count += 1
+            for c, sub_set in cmd_set.items():
+                result_set[c] = count
+                count_nest(sub_set, count)
+
+        for root, sub_commands in command_set.items():
+            root.sub_commands = get_sub_commands(sub_commands)
+            root.increment_nest(-1)
+        layer_num = -1
+        count_nest(command_set, layer_num)
+        for cmd, expected in result_set.items():
+            actual = cmd._layer
+            assert expected == actual
+
+    @pytest.mark.parametrize(
+        'command_set', [
+            {RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+            {RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+            {RootCommand(): {}, SecondCommand(): {}, ThirdCommand(): {}}
+        ]
+    )
+    def test_register_parent(self, command_set):
+        result_set = {}
+
+        def collect_ids(parent_ids, cmd_set):
+            for c, sub_set in cmd_set.items():
+                current_set = parent_ids | {id(c)}
+                result_set[c] = current_set
+                collect_ids(current_set, sub_set)
+
+        for root, sub_commands in command_set.items():
+            root.sub_commands = get_sub_commands(sub_commands)
+            root.register_parent(set())
+        collect_ids(set(), command_set)
+
+        for cmd, expected in result_set.items():
+            actual = cmd._parent_ids
+            assert expected == actual
+
+    @pytest.mark.parametrize(
+        'command_set', [
+            {RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+            {RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+            {RootCommand(): {}, SecondCommand(): {}, ThirdCommand(): {}}
+        ]
+    )
+    def test_add_command(self, command_set):
+        result_set = {}
+
+        def add_command(cmd, cmd_set):
+            result_set[cmd] = set()
+            for c, sub_set in cmd_set.items():
+                result_set[cmd].add(id(c))
+                cmd.add_command(c)
+                add_command(c, sub_set)
+
+        for root, sub_commands in command_set.items():
+            add_command(root, sub_commands)
+
+        for command, expected in result_set.items():
+            actual = command._sub_command_ids
+            assert actual == expected
+
+    @pytest.mark.parametrize(
+        'command_set', [
+            {RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+            {RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+        ]
+    )
+    def test_add_duplicate_command(self, command_set):
+        result_set = {}
+
+        def add_command(parent_cmds, cmd, cmd_set):
+            result_set[cmd] = parent_cmds + [cmd]
+            for c, sub_set in cmd_set.items():
+                cmd.add_command(c)
+                add_command(result_set[cmd], c, sub_set)
+
+        for root, sub_commands in command_set.items():
+            add_command([], root, sub_commands)
+
+        for command, parents in result_set.items():
+            for parent in parents:
+                with pytest.raises(CommandDuplicateError):
+                    command.add_command(parent)
+
+    @pytest.mark.parametrize(
+        'command_set,argv,expected', [
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root second third", ThirdCommand.value),
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root second", SecondCommand.value),
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root", RootCommand.value),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root second", SecondCommand.value),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root third", ThirdCommand.value),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root", RootCommand.value),
+        ]
+    )
+    def test_execute(self, command_set, argv, expected, capsys):
+
+        def add_command(cmd, cmd_set):
+            for c, sub_set in cmd_set.items():
+                cmd.add_command(c)
+                add_command(c, sub_set)
+
+        for root, sub_commands in command_set.items():
+            add_command(root, sub_commands)
+            root.execute(argv.split(' ')[1:])
+            stdout = capsys.readouterr()
+            actual = stdout.out.splitlines()[0]
+            assert actual == str(expected)
+
+    @pytest.mark.parametrize(
+        'command_set,argv,expected', [
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root second third --third invalid", "third"),
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root second --second 10", "second"),
+            ({RootCommand(): {SecondCommand(): {ThirdCommand(): {}}}},
+             "root --root", "root"),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root second --second 10", "second"),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root third --third invalid", "third"),
+            ({RootCommand(): {SecondCommand(): {}, ThirdCommand(): {}}},
+             "root --root", "root"),
+        ]
+    )
+    def test_violates_validation_argv(
+            self, command_set, argv, expected, caplog):
+
+        def add_command(cmd, cmd_set):
+            for c, sub_set in cmd_set.items():
+                cmd.add_command(c)
+                add_command(c, sub_set)
+
+        for root, sub_commands in command_set.items():
+            add_command(root, sub_commands)
+            root.execute(argv.split(' ')[1:])
+            out = caplog.record_tuples[0]
+            actual = out[1:]
+            assert actual == (logging.ERROR, expected)