Repository: cocotb/cocotb

Issue:
Provide friendlier handling of invalid COCOTB_LOG_LEVEL
When running with `COCOTB_LOG_LEVEL=debug` I get the shown stack trace.

We should be able to improve the user experience a bit by:
- Converting values to ALLCAPS, i.e. debug becomes DEBUG
- Provide a more helpful error message with allowed options instead of a stacktrace.


```
        sim_build/fifo 
     -.--ns INFO     cocotb.gpi                         ..mbed/gpi_embed.cpp:74   in set_program_name_in_venv        Did not detect Python virtual environment. Using system-wide Python interpreter
     -.--ns INFO     cocotb.gpi                         ../gpi/GpiCommon.cpp:105  in gpi_print_registered_impl       VPI registered
     -.--ns INFO     cocotb.gpi                         ..mbed/gpi_embed.cpp:245  in embed_sim_init                  Python interpreter initialized and cocotb loaded!
Traceback (most recent call last):
  File "/home/philipp/.local/lib/python3.8/site-packages/cocotb/__init__.py", line 175, in _initialise_testbench
    _setup_logging()
  File "/home/philipp/.local/lib/python3.8/site-packages/cocotb/__init__.py", line 76, in _setup_logging
    cocotb.log.default_config()
  File "/home/philipp/.local/lib/python3.8/site-packages/cocotb/log.py", line 95, in default_config
    log.setLevel(_default_log)
  File "/usr/lib64/python3.8/logging/__init__.py", line 1409, in setLevel
    self.level = _checkLevel(level)
  File "/usr/lib64/python3.8/logging/__init__.py", line 197, in _checkLevel
    raise TypeError("Level not an integer or a valid string: %r" % level)
TypeError: Level not an integer or a valid string: <function debug at 0x7f8653508430>
```


Discussion:

Resolving patch:
diff --git a/cocotb/log.py b/cocotb/log.py
--- a/cocotb/log.py
+++ b/cocotb/log.py
@@ -52,6 +52,9 @@
 _LINENO_CHARS   = 4  # noqa
 _FUNCNAME_CHARS = 31  # noqa
 
+# Default log level if not overwritten by the user.
+_COCOTB_LOG_LEVEL_DEFAULT = "INFO"
+
 
 def default_config():
     """ Apply the default cocotb log formatting to the root logger.
@@ -86,18 +89,25 @@ def default_config():
 
     # apply level settings for cocotb
     log = logging.getLogger('cocotb')
-    level = os.getenv("COCOTB_LOG_LEVEL", "INFO")
+
+    try:
+        # All log levels are upper case, convert the user input for convenience.
+        level = os.environ["COCOTB_LOG_LEVEL"].upper()
+    except KeyError:
+        level = _COCOTB_LOG_LEVEL_DEFAULT
+
     try:
-        _default_log = getattr(logging, level)
-    except AttributeError:
-        log.error("Unable to set logging level to %r" % level)
-        _default_log = logging.INFO
-    log.setLevel(_default_log)
+        log.setLevel(level)
+    except ValueError:
+        valid_levels = ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG')
+        raise ValueError("Invalid log level %r passed through the "
+                         "COCOTB_LOG_LEVEL environment variable. Valid log "
+                         "levels: %s" % (level, ', '.join(valid_levels)))
 
     # Notify GPI of log level, which it uses as an optimization to avoid
     # calling into Python.
     from cocotb import simulator
-    simulator.log_level(_default_log)
+    simulator.log_level(log.getEffectiveLevel())
 
 
 class SimBaseLog(logging.getLoggerClass()):


Test patch:
diff --git a/tests/test_cases/test_cocotb/test_logging.py b/tests/test_cases/test_cocotb/test_logging.py
--- a/tests/test_cases/test_cocotb/test_logging.py
+++ b/tests/test_cases/test_cocotb/test_logging.py
@@ -4,9 +4,14 @@
 """
 Tests for the cocotb logger
 """
+
 import cocotb
+import cocotb.log
 from cocotb.triggers import Timer
+
+from common import assert_raises
 import logging
+import os
 
 
 class StrCallCounter(object):
@@ -39,3 +44,39 @@ def test_logging_with_args(dut):
     dut._log.warning("Testing multiple line\nmessage")
 
     yield Timer(100)  # Make it do something with time
+
+
+@cocotb.test()
+def test_logging_default_config(dut):
+    # The cocotb.log module is shadowed by an instance of
+    # cocotb.log.SimBaseLog()
+    from cocotb.log import default_config as log_default_config
+
+    cocotb_log = logging.getLogger('cocotb')
+
+    # Save pre-test configuration
+    log_level_prev = cocotb_log.level
+    os_environ_prev = os.environ.copy()
+
+    try:
+        # Set a valid log level
+        os.environ['COCOTB_LOG_LEVEL'] = 'DEBUG'
+        log_default_config()
+        assert cocotb_log.level == logging.DEBUG, cocotb_log.level
+
+        # Try to set log level to an invalid log level
+        os.environ['COCOTB_LOG_LEVEL'] = 'INVALID_LOG_LEVEL'
+        with assert_raises(ValueError):
+            log_default_config()
+
+        # Try to set log level to a valid log level with wrong capitalization
+        os.environ['COCOTB_LOG_LEVEL'] = 'error'
+        log_default_config()
+        assert cocotb_log.level == logging.ERROR, cocotb_log.level
+
+    finally:
+        # Restore pre-test configuration
+        os.environ = os_environ_prev
+        cocotb_log.level = log_level_prev
+
+    yield Timer(1)