Repository: nose-devs/nose2

Issue:
verbosity is ignored when set in configuration file or as an argument to nose2.main()
This is, I think, related to, but is not the same as, issue #115. After seeing no affect when I set verbosity in my .cfg file or passed it as an argument from nose2.main(), I used a debugger to dive into the code and it seemed to lose its verbosity somewhere along the line and revert to using the session's default verbosity of 1.

I haven't been able to figure out what value verbosity takes when nose2 is run from the command line with "-v", though that does work.


Comments:

Patch:
diff --git a/nose2/main.py b/nose2/main.py
index 67d5483..ea533c6 100644
--- a/nose2/main.py
+++ b/nose2/main.py
@@ -155,9 +155,17 @@ class PluggableTestProgram(unittest.TestProgram):
             dest='exclude_plugins', default=[],
             help="Do not load this plugin module")
         self.argparse.add_argument(
-            '--verbose', '-v', action='count', default=0, help="print test case names and statuses")
-        self.argparse.add_argument('--quiet', action='store_const',
-                                   dest='verbose', const=0)
+            '--verbosity', type=int,
+            help=("Set starting verbosity level (int). "
+                  "Applies before -v and -q"))
+        self.argparse.add_argument(
+            '--verbose', '-v', action='count', default=0,
+            help=("Print test case names and statuses. "
+                  "Use multiple '-v's for higher verbosity."))
+        self.argparse.add_argument(
+            '--quiet', '-q', action='count', default=0, dest='quiet',
+            help=("Reduce verbosity. Multiple '-q's result in "
+                  "lower verbosity."))
         self.argparse.add_argument(
             '--log-level', default=logging.WARN,
             help='Set logging level for message logged to console.')
@@ -172,13 +180,13 @@ class PluggableTestProgram(unittest.TestProgram):
         self.session.logLevel = util.parse_log_level(cfg_args.log_level)
         logging.basicConfig(level=self.session.logLevel)
         log.debug('logging initialized %s', cfg_args.log_level)
-        if cfg_args.verbose:
-            self.session.verbosity += cfg_args.verbose
-        self.session.startDir = cfg_args.start_dir
         if cfg_args.top_level_directory:
             self.session.topLevelDir = cfg_args.top_level_directory
         self.session.loadConfigFiles(*self.findConfigFiles(cfg_args))
-        self.session.setStartDir()
+        # set verbosity from config + opts
+        self.session.setVerbosity(
+            cfg_args.verbosity, cfg_args.verbose, cfg_args.quiet)
+        self.session.setStartDir(args_start_dir=cfg_args.start_dir)
         self.session.prepareSysPath()
         if cfg_args.load_plugins:
             self.defaultPlugins.extend(cfg_args.plugins)
diff --git a/nose2/session.py b/nose2/session.py
index ee47c6d..e6c633c 100644
--- a/nose2/session.py
+++ b/nose2/session.py
@@ -72,7 +72,7 @@ class Session(object):
 
     """
     configClass = config.Config
-    
+
     def __init__(self):
         self.argparse = argparse.ArgumentParser(prog='nose2', add_help=False)
         self.pluginargs = self.argparse.add_argument_group(
@@ -81,6 +81,8 @@ class Session(object):
         self.config = configparser.ConfigParser()
         self.hooks = events.PluginInterface()
         self.plugins = []
+        # this will be reset later, whenever handleCfgArgs happens, but it
+        # starts at 1 so that it always has a non-negative integer value
         self.verbosity = 1
         self.startDir = None
         self.topLevelDir = None
@@ -183,9 +185,32 @@ class Session(object):
                 log.debug("Register method %s for plugin %s", method, plugin)
                 self.hooks.register(method, plugin)
 
-    def setStartDir(self):
-        if self.startDir is None:
-            self.startDir = self.unittest.as_str('start-dir', '.')
+    def setVerbosity(self, args_verbosity, args_verbose, args_quiet):
+        """
+        Determine verbosity from various (possibly conflicting) sources of info
+
+        :param args_verbosity: The --verbosity argument value
+        :param args_verbose: count of -v options
+        :param args_quiet: count of -q options
+
+        start with config, override with any given --verbosity, then adjust
+        up/down with -vvv -qq, etc
+        """
+        self.verbosity = self.unittest.as_int('verbosity', 1)
+        if args_verbosity is not None:
+            self.verbosity = args_verbosity
+        # adjust up or down, depending on the difference of these counts
+        self.verbosity += args_verbose - args_quiet
+        # floor the value at 0 -- verbosity is always a non-negative integer
+        self.verbosity = max(self.verbosity, 0)
+
+    def setStartDir(self, args_start_dir=None):
+        """
+        start dir comes from config and may be overridden by an argument
+        """
+        self.startDir = self.unittest.as_str('start-dir', '.')
+        if args_start_dir is not None:
+            self.startDir = args_start_dir
 
     def prepareSysPath(self):
         """Add code directories to sys.path"""
@@ -220,7 +245,8 @@ class Session(object):
     def isPluginLoaded(self, pluginName):
         """Returns ``True`` if a given plugin is loaded.
 
-        :param pluginName: the name of the plugin module: e.g. "nose2.plugins.layers".
+        :param pluginName: the name of the plugin module:
+                           e.g. "nose2.plugins.layers".
 
         """
         for plugin in self.plugins:


Test patch:
diff --git a/nose2/tests/functional/test_verbosity.py b/nose2/tests/functional/test_verbosity.py
new file mode 100644
index 0000000..e297e9c
--- /dev/null
+++ b/nose2/tests/functional/test_verbosity.py
@@ -0,0 +1,77 @@
+from nose2.tests._common import FunctionalTestCase
+
+_SUFFIX = (
+    "\n----------------------------------------------------------------------"
+    "\nRan 1 test "
+)
+
+Q_TEST_PATTERN = r"(?<!\.)(?<!ok)" + _SUFFIX
+MID_TEST_PATTERN = r"\." + _SUFFIX
+V_TEST_PATTERN = r"test \(__main__\.Test\) \.\.\. ok" + "\n" + _SUFFIX
+
+
+class TestVerbosity(FunctionalTestCase):
+    def test_no_modifier(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py")
+        self.assertTestRunOutputMatches(proc, stderr=MID_TEST_PATTERN)
+
+    def test_one_v(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-v")
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+
+    def test_one_q(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-q")
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)
+
+    def test_one_q_one_v(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-q", "-v")
+        self.assertTestRunOutputMatches(proc, stderr=MID_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-qv")
+        self.assertTestRunOutputMatches(proc, stderr=MID_TEST_PATTERN)
+
+    def test_one_q_two_vs(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-q", "-v", "-v")
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-qvv")
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+
+    def test_one_v_two_qs(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-v", "-q", "-q")
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "-vqq")
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)
+
+    def test_explicit_verbosity(self):
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "--verbosity", "0")
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "--verbosity", "1")
+        self.assertTestRunOutputMatches(proc, stderr=MID_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "--verbosity", "2")
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+
+        proc = self.runModuleAsMain("scenario/one_test/tests.py", "--verbosity", "-1")
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)
+
+    def test_tweaked_explicit_verbosity(self):
+        proc = self.runModuleAsMain(
+            "scenario/one_test/tests.py", "--verbosity", "0", "-vv"
+        )
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+        proc = self.runModuleAsMain(
+            "scenario/one_test/tests.py", "-vv", "--verbosity", "0"
+        )
+        self.assertTestRunOutputMatches(proc, stderr=V_TEST_PATTERN)
+
+        proc = self.runModuleAsMain(
+            "scenario/one_test/tests.py", "--verbosity", "2", "-q"
+        )
+        self.assertTestRunOutputMatches(proc, stderr=MID_TEST_PATTERN)
+        proc = self.runModuleAsMain(
+            "scenario/one_test/tests.py", "-q", "--verbosity", "2", "-q"
+        )
+        self.assertTestRunOutputMatches(proc, stderr=Q_TEST_PATTERN)