Unified command execution and unit tests

- All arguments inits moved to own clases
- Added unified way to execute commands
- Unit test structure and very basic tests
- Command line script to test coverage

Change-Id: I10bc973776595779b563b84548d46367bcd0886f
Related-PROD: PROD-28199
diff --git a/cfg_checker/cfg_check.py b/cfg_checker/cfg_check.py
index 00e40d2..22125f6 100644
--- a/cfg_checker/cfg_check.py
+++ b/cfg_checker/cfg_check.py
@@ -1,38 +1,17 @@
-import argparse
 import os
 import sys
-import traceback
 from logging import DEBUG, INFO
 
+from cfg_checker.cli.command import execute_command
+from cfg_checker.cli.network import init_network_parser
+from cfg_checker.cli.package import init_package_parser
+from cfg_checker.cli.reclass import init_reclass_parser
 from cfg_checker.common import config, logger, logger_cli
-from cfg_checker.common.exception import CheckerException
-
+from cfg_checker.helpers.args_utils import MyParser
 
 pkg_dir = os.path.dirname(__file__)
 pkg_dir = os.path.normpath(pkg_dir)
 
-commands = {
-    'packages': ['report'],
-    'network': ['check', 'report'],
-    'reclass': ['list', 'diff']
-}
-
-
-class MyParser(argparse.ArgumentParser):
-    def error(self, message):
-        sys.stderr.write('Error: {0}\n\n'.format(message))
-        self.print_help()
-
-
-def help_message():
-    print"""
-    Please, use following examples to generate info reports:\n
-         cfg_checker packages report\n
-         cfg_checker network check\n
-         cfg_checker network report\n
-    """
-    return
-
 
 def config_check_entrypoint():
     """
@@ -44,19 +23,6 @@
     # Main entrypoint
     parser = MyParser(prog="# Mirantis Cloud configuration checker")
 
-    # Parsers (each parser can have own arguments)
-    # - subparsers (command)
-    #   |- pkg_parser
-    #   |  - pkg_subparsers (type)
-    #   |    - pkg_report_parser (default func - pkg_check)
-    #   |- net_parser
-    #   |  - net_subparsers (type)
-    #   |    - net_check_parser (default func - net_check)
-    #   |    - net_report_parser (default func - net_report)
-    #    - reclass_parser
-    #      - reclass_list (default func - reclass_list)
-    #      - reclass_compare (default func - reclass_diff)
-
     parser.add_argument(
         "-d",
         "--debug",
@@ -70,105 +36,34 @@
         help="Use sudo for getting salt creds"
     )
     subparsers = parser.add_subparsers(dest='command')
-    # packages
+
+    # package
     pkg_parser = subparsers.add_parser(
         'packages',
         help="Package versions check (Candidate vs Installed)"
     )
-    pkg_subparsers = pkg_parser.add_subparsers(dest='type')
-
-    pkg_report_parser = pkg_subparsers.add_parser(
-        'report',
-        help="Report package versions to HTML file"
-    )
-    pkg_report_parser.add_argument(
-        '--full',
-        action="store_true", default=False,
-        help="HTML report will have all of the packages, not just errors"
-    )
-    pkg_report_parser.add_argument(
-        '--html',
-        metavar='packages_html_filename',
-        help="HTML filename to save report"
-    )
-    pkg_report_parser.add_argument(
-        '--csv',
-        metavar='packages_csv_filename',
-        help="CSV filename to save report"
-    )
+    init_package_parser(pkg_parser)
 
     # networking
     net_parser = subparsers.add_parser(
         'network',
         help="Network infrastructure checks and reports"
     )
-    net_subparsers = net_parser.add_subparsers(dest='type')
-
-    net_check_parser = net_subparsers.add_parser(
-        'check',
-        help="Do network check and print the result"
-    )
-
-    net_check_parser.add_argument(
-        '--detailed',
-        action="store_true", default=False,
-        help="Print error details after summary"
-    )
-
-    net_report_parser = net_subparsers.add_parser(
-        'report',
-        help="Generate network check report"
-    )
-
-    net_report_parser.add_argument(
-        '--html',
-        metavar='network_html_filename',
-        help="HTML filename to save report"
-    )
+    init_network_parser(net_parser)
 
     # reclass
     reclass_parser = subparsers.add_parser(
         'reclass',
         help="Reclass related checks and reports"
     )
-    reclass_subparsers = reclass_parser.add_subparsers(dest='type')
-    reclass_list_parser = reclass_subparsers.add_parser(
-        'list',
-        help="List models available to compare"
-    )
-    reclass_list_parser.add_argument(
-        "-p",
-        "--models-path",
-        default="/srv/salt/",
-        help="Global path to search models in"
-    )
-
-    reclass_diff_parser = reclass_subparsers.add_parser(
-        'diff',
-        help="List models available to compare"
-    )
-    reclass_diff_parser.add_argument(
-        "--model1",
-        required=True,
-        help="Model A <path>. Model name is the folder name"
-    )
-    reclass_diff_parser.add_argument(
-        "--model2",
-        required=True,
-        help="Model B <path>. Model name is the folder name"
-    )
-    reclass_diff_parser.add_argument(
-        '--html',
-        metavar='reclass_html_filename',
-        help="HTML filename to save report"
-    )
+    init_reclass_parser(reclass_parser)
 
     # parse arguments
     try:
         args = parser.parse_args()
     except TypeError:
         logger_cli.info("\n# Please, check arguments")
-        return
+        sys.exit(0)
 
     # Pass externally configured values
     config.ssh_uses_sudo = args.sudo
@@ -179,51 +74,11 @@
     else:
         logger_cli.setLevel(INFO)
 
-    # Validate the commands
-    # check command
-    if args.command not in commands:
-        logger_cli.info("\n# Please, type a command listed above")
-        return
-    elif args.type not in commands[args.command]:
-        # check type
-        logger_cli.info(
-            "\n# Please, select '{}' command type listed above".format(
-                args.command
-            )
-        )
-        return
-    else:
-        # form function name to call
-        _method_name = "do_" + args.type
-        _target_module = __import__(
-            "cfg_checker.modules."+args.command,
-            fromlist=[""]
-        )
-        _method = getattr(_target_module, _method_name)
-
     # Execute the command
-    result = _method(args)
-
+    result = execute_command(args, args.command)
     logger.debug(result)
-
-
-def cli_main():
-    try:
-        config_check_entrypoint()
-    except CheckerException as e:
-        logger_cli.error("\nERROR: {}".format(
-            e.message
-        ))
-
-        exc_type, exc_value, exc_traceback = sys.exc_info()
-        logger_cli.debug("\n{}".format(
-            "".join(traceback.format_exception(
-                exc_type,
-                exc_value,
-                exc_traceback
-            ))
-        ))
+    sys.exit(result)
 
 
 if __name__ == '__main__':
-    cli_main()
+    config_check_entrypoint()
diff --git a/cfg_checker/cli/command.py b/cfg_checker/cli/command.py
new file mode 100644
index 0000000..0a9c2a2
--- /dev/null
+++ b/cfg_checker/cli/command.py
@@ -0,0 +1,59 @@
+import sys
+import traceback
+
+from cfg_checker.common import logger_cli
+from cfg_checker.common.exception import CheckerException
+
+# TODO: auto-create types for each command
+package_command_types = ['report']
+network_command_types = ['check', 'report']
+reclass_command_types = ['list', 'diff']
+
+commands = {
+    'packages': package_command_types,
+    'network': network_command_types,
+    'reclass': reclass_command_types
+}
+
+
+def execute_command(args, command):
+    # Validate the commands
+    # check command
+    if command not in commands:
+        logger_cli.info("\n# Please, type a command listed above")
+        return 0
+    elif args.type not in commands[command]:
+        # check type
+        logger_cli.info(
+            "\n# Please, select '{}' command type listed above".format(
+                command
+            )
+        )
+        return 0
+    else:
+        # form function name to call
+        _method_name = "do_" + args.type
+        _target_module = __import__(
+            "cfg_checker.modules."+command,
+            fromlist=[""]
+        )
+        _method = getattr(_target_module, _method_name)
+
+    # Execute the command
+    try:
+        _method(args)
+        return 0
+    except CheckerException as e:
+        logger_cli.error("\nERROR: {}".format(
+            e.message
+        ))
+
+        exc_type, exc_value, exc_traceback = sys.exc_info()
+        logger_cli.debug("\n{}".format(
+            "".join(traceback.format_exception(
+                exc_type,
+                exc_value,
+                exc_traceback
+            ))
+        ))
+        return 1
diff --git a/cfg_checker/cli/network.py b/cfg_checker/cli/network.py
index 4d6d0c4..b53311d 100644
--- a/cfg_checker/cli/network.py
+++ b/cfg_checker/cli/network.py
@@ -1,14 +1,59 @@
-from cfg_checker.modules.network.checker import NetworkChecker
+import sys
+
+from cfg_checker.common import config, logger, logger_cli
+from cfg_checker.helpers.args_utils import MyParser
+
+from command import execute_command
+
+
+def init_network_parser(_parser):
+    # network subparser
+    net_subparsers = _parser.add_subparsers(dest='type')
+
+    net_check_parser = net_subparsers.add_parser(
+        'check',
+        help="Do network check and print the result"
+    )
+
+    net_check_parser.add_argument(
+        '--detailed',
+        action="store_true", default=False,
+        help="Print error details after summary"
+    )
+
+    net_report_parser = net_subparsers.add_parser(
+        'report',
+        help="Generate network check report"
+    )
+
+    net_report_parser.add_argument(
+        '--html',
+        metavar='network_html_filename',
+        help="HTML filename to save report"
+    )
+
+    return _parser
+
+
+def cli_network():
+    net_parser = MyParser("# Mirantis Cloud Network checker")
+    init_network_parser(net_parser)
+
+    # parse arguments
+    try:
+        args = net_parser.parse_args()
+    except TypeError:
+        logger_cli.info("\n# Please, check arguments")
+        sys.exit(0)
+
+    # force use of sudo
+    config.ssh_uses_sudo = True
+
+    # Execute the command
+    result = execute_command(args, 'network')
+    logger.debug(result)
+    sys.exit(result)
+
 
 if __name__ == '__main__':
-    # init connection to salt and collect minion data
-    cl = NetworkChecker()
-
-    # collect data on installed packages
-    cl.collect_network_info()
-
-    # diff installed and candidates
-    # cl.collect_packages()
-
-    # report it
-    cl.create_html_report("./pkg_versions.html")
+    cli_network()
diff --git a/cfg_checker/cli/package.py b/cfg_checker/cli/package.py
index 74166be..5fe7e0b 100644
--- a/cfg_checker/cli/package.py
+++ b/cfg_checker/cli/package.py
@@ -1,15 +1,57 @@
-from cfg_checker.modules.packages.checker import CloudPackageChecker
+import sys
+
+from cfg_checker.common import config, logger, logger_cli
+from cfg_checker.helpers.args_utils import MyParser
+
+from command import execute_command
+
+
+def init_package_parser(_parser):
+    # packages subparser
+    pkg_subparsers = _parser.add_subparsers(dest='type')
+
+    pkg_report_parser = pkg_subparsers.add_parser(
+        'report',
+        help="Report package versions to HTML file"
+    )
+    pkg_report_parser.add_argument(
+        '--full',
+        action="store_true", default=False,
+        help="HTML report will have all of the packages, not just errors"
+    )
+    pkg_report_parser.add_argument(
+        '--html',
+        metavar='packages_html_filename',
+        help="HTML filename to save report"
+    )
+    pkg_report_parser.add_argument(
+        '--csv',
+        metavar='packages_csv_filename',
+        help="CSV filename to save report"
+    )
+
+    return _parser
+
+
+def cli_package():
+    pkg_parser = MyParser("# Mirantis Cloud Package checker")
+    init_package_parser(pkg_parser)
+
+    # parse arguments
+    try:
+        args = pkg_parser.parse_args()
+    except TypeError:
+        logger_cli.info("\n# Please, check arguments")
+        sys.exit(0)
+
+    # force use of sudo
+    config.ssh_uses_sudo = True
+
+    # Execute the command
+    result = execute_command(args, 'packages')
+    logger.debug(result)
+    sys.exit(result)
 
 
 if __name__ == '__main__':
-    # init connection to salt and collect minion data
-    cl = CloudPackageChecker()
-
-    # collect data on installed packages
-    cl.collect_installed_packages()
-
-    # diff installed and candidates
-    # cl.collect_packages()
-
-    # report it
-    cl.create_html_report("./pkg_versions.html")
+    cli_package()
diff --git a/cfg_checker/cli/reclass.py b/cfg_checker/cli/reclass.py
index 2959bf3..fc5961a 100644
--- a/cfg_checker/cli/reclass.py
+++ b/cfg_checker/cli/reclass.py
@@ -1,6 +1,67 @@
-from cfg_checker.modules.reclass.comparer import ModelComparer
+import sys
+
+from cfg_checker.common import config, logger, logger_cli
+from cfg_checker.helpers.args_utils import MyParser
+
+from command import execute_command
+
+
+def init_reclass_parser(_parser):
+    # reclass subparsers
+    reclass_subparsers = _parser.add_subparsers(dest='type')
+    reclass_list_parser = reclass_subparsers.add_parser(
+        'list',
+        help="List models available to compare"
+    )
+    reclass_list_parser.add_argument(
+        "-p",
+        "--models-path",
+        default="/srv/salt/",
+        help="Global path to search models in"
+    )
+
+    reclass_diff_parser = reclass_subparsers.add_parser(
+        'diff',
+        help="List models available to compare"
+    )
+    reclass_diff_parser.add_argument(
+        "--model1",
+        required=True,
+        help="Model A <path>. Model name is the folder name"
+    )
+    reclass_diff_parser.add_argument(
+        "--model2",
+        required=True,
+        help="Model B <path>. Model name is the folder name"
+    )
+    reclass_diff_parser.add_argument(
+        '--html',
+        metavar='reclass_html_filename',
+        help="HTML filename to save report"
+    )
+
+    return _parser
+
+
+def cli_reclass():
+    net_parser = MyParser("# Mirantis Cloud Reclass comparer")
+    init_reclass_parser(net_parser)
+
+    # parse arguments
+    try:
+        args = net_parser.parse_args()
+    except TypeError:
+        logger_cli.info("\n# Please, check arguments")
+        sys.exit(0)
+
+    # force use of sudo
+    config.ssh_uses_sudo = True
+
+    # Execute the command
+    result = execute_command(args, 'reclass')
+    logger.debug(result)
+    sys.exit(result)
+
 
 if __name__ == "__main__":
-    # Execute the comparison using argv params
-
-    pass
+    cli_reclass()
diff --git a/cfg_checker/helpers/args_utils.py b/cfg_checker/helpers/args_utils.py
index f8453e4..daf55b1 100644
--- a/cfg_checker/helpers/args_utils.py
+++ b/cfg_checker/helpers/args_utils.py
@@ -1,8 +1,16 @@
+import argparse
 import os
+import sys
 
 from cfg_checker.common.exception import ConfigException
 
 
+class MyParser(argparse.ArgumentParser):
+    def error(self, message):
+        sys.stderr.write('Error: {0}\n\n'.format(message))
+        self.print_help()
+
+
 def get_arg(args, str_arg):
     _attr = getattr(args, str_arg)
     if _attr:
diff --git a/cfg_checker/modules/packages/versions.py b/cfg_checker/modules/packages/versions.py
index 9352dd6..9737d80 100644
--- a/cfg_checker/modules/packages/versions.py
+++ b/cfg_checker/modules/packages/versions.py
@@ -1,7 +1,8 @@
 import csv
 import os
 
-from cfg_checker.common import config, const, logger_cli, pkg_dir
+from cfg_checker.common import config, const, logger_cli
+from cfg_checker.common.settings import pkg_dir
 
 
 class PkgVersions(object):
diff --git a/cover.sh b/cover.sh
new file mode 100644
index 0000000..49c73e5
--- /dev/null
+++ b/cover.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+coverage run --source cfg_checker setup.py test && coverage report
diff --git a/requirements-test.txt b/requirements-test.txt
new file mode 100644
index 0000000..9cd0e0a
--- /dev/null
+++ b/requirements-test.txt
@@ -0,0 +1,3 @@
+six
+pyyaml
+coverage
diff --git a/setup.py b/setup.py
index 42ca20f..248ceb2 100644
--- a/setup.py
+++ b/setup.py
@@ -23,10 +23,10 @@
 
 entry_points = {
     "console_scripts": [
-        "mcp-checker = cfg_checker.cfg_check:cli_main",
-        "package-report = cfg_checker.cli.package",
-        "network-check = cfg_checker.cli.network",
-        "reclass-compare = cfg_checker.cli.reclass"
+        "mcp-checker = cfg_checker.cfg_check:config_check_entrypoint",
+        "mcp-checker-package = cfg_checker.cli.package:cli_package",
+        "mcp-checker-network = cfg_checker.cli.network:cli_network",
+        "mcp-checker-reclass = cfg_checker.cli.reclass:cli_reclass"
     ]
 }
 
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..c4a627f
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,29 @@
+import contextlib
+import io
+import sys
+import unittest
+
+
+class CfgCheckerTestBase(unittest.TestCase):
+    dummy_base_var = 0
+
+    def _safe_import_module(self, _str):
+        _import_msg = ""
+        _module = None
+
+        try:
+            _module = __import__(_str)
+        except ImportError as e:
+            _import_msg = e.message
+
+        return _import_msg, _module
+
+    @contextlib.contextmanager
+    def redirect_output(self):
+        save_stdout = sys.stdout
+        save_stderr = sys.stderr
+        sys.stdout = io.BytesIO()
+        sys.stderr = io.BytesIO()
+        yield
+        sys.stdout = save_stdout
+        sys.stderr = save_stderr
diff --git a/tests/test_entrypoints.py b/tests/test_entrypoints.py
new file mode 100644
index 0000000..0b87e6d
--- /dev/null
+++ b/tests/test_entrypoints.py
@@ -0,0 +1,99 @@
+from test_base import CfgCheckerTestBase
+
+
+class TestEntrypoints(CfgCheckerTestBase):
+    def test_entry_mcp_checker(self):
+        _module_name = 'cfg_checker.cfg_check'
+        with self.redirect_output():
+            _msg, _m = self._safe_import_module(_module_name)
+
+        self.assertEqual(
+            len(_msg),
+            0,
+            "Error importing '{}': {}".format(
+                _module_name,
+                _msg
+            )
+        )
+
+        with self.redirect_output():
+            with self.assertRaises(SystemExit) as ep:
+                _m.cfg_check.config_check_entrypoint()
+
+        self.assertEqual(
+            ep.exception.code,
+            0,
+            "mcp-checker has non-zero exit-code"
+        )
+
+    def test_entry_packages(self):
+        _module_name = 'cfg_checker.cli.package'
+        with self.redirect_output():
+            _msg, _m = self._safe_import_module(_module_name)
+
+        self.assertEqual(
+            len(_msg),
+            0,
+            "Error importing '{}': {}".format(
+                _module_name,
+                _msg
+            )
+        )
+
+        with self.redirect_output():
+            with self.assertRaises(SystemExit) as ep:
+                _m.cli.package.cli_package()
+
+        self.assertEqual(
+            ep.exception.code,
+            0,
+            "packages has non-zero exit code"
+        )
+
+    def test_entry_network(self):
+        _module_name = 'cfg_checker.cli.network'
+        with self.redirect_output():
+            _msg, _m = self._safe_import_module(_module_name)
+
+        self.assertEqual(
+            len(_msg),
+            0,
+            "Error importing '{}': {}".format(
+                _module_name,
+                _msg
+            )
+        )
+
+        with self.redirect_output():
+            with self.assertRaises(SystemExit) as ep:
+                _m.cli.network.cli_network()
+
+        self.assertEqual(
+            ep.exception.code,
+            0,
+            "network has non-zero exit-code"
+        )
+
+    def test_entry_reclass(self):
+        _module_name = 'cfg_checker.cli.reclass'
+        with self.redirect_output():
+            _msg, _m = self._safe_import_module(_module_name)
+
+        self.assertEqual(
+            len(_msg),
+            0,
+            "Error importing '{}': {}".format(
+                _module_name,
+                _msg
+            )
+        )
+
+        with self.redirect_output():
+            with self.assertRaises(SystemExit) as ep:
+                _m.cli.reclass.cli_reclass()
+
+        self.assertEqual(
+            ep.exception.code,
+            0,
+            "reclass has non-zero exit-code"
+        )