Repository: learningequality/kolibri

Issue:
support for options aliases and deprecation

## Observed behavior

Follow-up from #8050 which fixed #5754

* In https://github.com/learningequality/kolibri/pull/8050/files#r626688907 there was discussion about adding better warnings for the deprecated `CHERRYPY_START` option
* the `CENTRAL_CONTENT_DOWNLOAD_BASE_URL` and `KOLIBRI_LISTEN_PORT` aliases should be deprecated


## Expected behavior

The `base_option_spec` object could  contain three new attributes, `deprecated` (bool) and `deprecated_aliases` (list), and `deprecated_envvars` (list). This would allow a path for renaming and deprecating options while maintaining backward compatibility and providing appropriate warnings to users:

```diff
base_option_spec = {
    # ...
    "Server": {
        "CHERRYPY_START": {
            "type": "boolean",
            "default": True,
+            "deprecated": True,
            "description": "DEPRECATED - do not use this option, use the 'kolibri services' command instead.",
        },
        # ...
    },
    # ...
    "Urls": {
        "CENTRAL_CONTENT_BASE_URL": {
            "type": "string",
            "default": "https://studio.learningequality.org",
-            "envvars": ("CENTRAL_CONTENT_DOWNLOAD_BASE_URL",),
+            "deprecated_envvars": ("CENTRAL_CONTENT_DOWNLOAD_BASE_URL",),
            "description": """
                URL to use as the default source for content import.
                Slightly counterintuitively this will still be displayed in the UI as 'import from Kolibri Studio'.
            """,
        },
        # ...
    },
    # ...
    "Deployment": {
        "HTTP_PORT": {
            "type": "port",
            "default": 8080,
-            "envvars": (
-                "KOLIBRI_HTTP_PORT",
-                "KOLIBRI_LISTEN_PORT",
-            ),
+            "deprecated_aliases": ("LISTEN_PORT",),
            "description": "Sets the port that Kolibri will serve on. This can be further overridden by command line arguments.",
        },
        # ...
    },
    # ...
}
```

## User-facing consequences

Help users to migrate to consistently-named and canonical options and env vars.


## Context

0.14


Discussion:

Patch:
diff --git a/kolibri/core/webpack/hooks.py b/kolibri/core/webpack/hooks.py
index 5eb34a6405..8b4e01fd44 100644
--- a/kolibri/core/webpack/hooks.py
+++ b/kolibri/core/webpack/hooks.py
@@ -22,7 +22,6 @@ from functools import partial
 from django.conf import settings
 from django.contrib.staticfiles.finders import find as find_staticfiles
 from django.contrib.staticfiles.storage import staticfiles_storage
-from django.core.cache import caches
 from django.utils.functional import cached_property
 from django.utils.safestring import mark_safe
 from django.utils.six.moves.urllib.request import url2pathname
@@ -34,19 +33,14 @@ from six import text_type
 
 from kolibri.plugins import hooks
 
-# Use the cache specifically for built files
-# Only reference the specific cache inside methods
-# to allow this file to be imported without initiating
-# Django settings configuration.
-CACHE_NAMESPACE = "built_files"
 
 IGNORE_PATTERNS = (re.compile(I) for I in [r".+\.hot-update.js", r".+\.map"])
 
 
 class WebpackError(Exception):
-    def __init__(self, message, extra_info={}):
-        self.extra_info = extra_info
-        return Exception.__init__(self, message)
+    def __init__(self, message, extra_info=None):
+        self.extra_info = extra_info or {}
+        Exception.__init__(self, message)
 
 
 logger = logging.getLogger(__name__)
@@ -100,45 +94,37 @@ class WebpackBundleHook(hooks.KolibriHook):
         :returns: A dict of the data contained in the JSON files which are
           written by Webpack.
         """
-        cache_key = "json_stats_file_cache_{unique_id}".format(unique_id=self.unique_id)
-        stats_file_content = caches[CACHE_NAMESPACE].get(cache_key)
+        STATS_ERR = "Error accessing stats file '{}': {}"
 
-        if not stats_file_content or getattr(settings, "DEVELOPER_MODE", False):
+        try:
+            with io.open(self._stats_file, mode="r", encoding="utf-8") as f:
+                stats = json.load(f)
+        except IOError as e:
+            raise WebpackError(STATS_ERR.format(self._stats_file, e))
 
-            STATS_ERR = "Error accessing stats file '{}': {}"
+        if getattr(settings, "DEVELOPER_MODE", False):
+            timeout = 0
 
-            try:
-                with io.open(self._stats_file, mode="r", encoding="utf-8") as f:
-                    stats = json.load(f)
-            except IOError as e:
-                raise WebpackError(STATS_ERR.format(self._stats_file, e))
+            while stats["status"] == "compiling":
+                time.sleep(0.1)
+                timeout += 0.1
 
-            if getattr(settings, "DEVELOPER_MODE", False):
-                timeout = 0
-
-                while stats["status"] == "compiling":
-                    time.sleep(0.1)
-                    timeout += 0.1
-
-                    try:
-                        with io.open(self._stats_file, mode="r", encoding="utf-8") as f:
-                            stats = json.load(f)
-                    except IOError as e:
-                        raise WebpackError(STATS_ERR.format(self._stats_file, e))
+                try:
+                    with io.open(self._stats_file, mode="r", encoding="utf-8") as f:
+                        stats = json.load(f)
+                except IOError as e:
+                    raise WebpackError(STATS_ERR.format(self._stats_file, e))
 
-                    if timeout >= 5:
-                        raise WebpackError("Compilation still in progress")
+                if timeout >= 5:
+                    raise WebpackError("Compilation still in progress")
 
-                if stats["status"] == "error":
-                    raise WebpackError("Compilation has errored", stats)
+            if stats["status"] == "error":
+                raise WebpackError("Compilation has errored", stats)
 
-            stats_file_content = {
-                "files": stats.get("chunks", {}).get(self.unique_id, []),
-                "hasMessages": stats.get("messages", False),
-            }
-            # Don't invalidate during runtime.
-            # Might need to change this if we move to a different cache backend.
-            caches[CACHE_NAMESPACE].set(cache_key, stats_file_content, None)
+        stats_file_content = {
+            "files": stats.get("chunks", {}).get(self.unique_id, []),
+            "hasMessages": stats.get("messages", False),
+        }
 
         return stats_file_content
 
@@ -209,17 +195,11 @@ class WebpackBundleHook(hooks.KolibriHook):
 
     def frontend_messages(self):
         lang_code = get_language()
-        cache_key = "json_message_file_cache_{unique_id}_{lang}".format(
-            unique_id=self.unique_id, lang=lang_code
-        )
-        message_file_content = caches[CACHE_NAMESPACE].get(cache_key, {})
-        if not message_file_content or getattr(settings, "DEVELOPER_MODE", False):
-            frontend_message_file = self.frontend_message_file(lang_code)
-            if frontend_message_file:
-                with io.open(frontend_message_file, mode="r", encoding="utf-8") as f:
-                    message_file_content = json.load(f)
-                caches[CACHE_NAMESPACE].set(cache_key, message_file_content, None)
-        return message_file_content
+        frontend_message_file = self.frontend_message_file(lang_code)
+        if frontend_message_file:
+            with io.open(frontend_message_file, mode="r", encoding="utf-8") as f:
+                message_file_content = json.load(f)
+            return message_file_content
 
     def sorted_chunks(self):
         bidi = get_language_info(get_language())["bidi"]
@@ -349,25 +329,21 @@ class WebpackBundleHook(hooks.KolibriHook):
         """
         Reads file contents using given `charset` and returns it as text.
         """
-        cache_key = "inline_static_file_content_{url}".format(url=url)
-        content = caches[CACHE_NAMESPACE].get(cache_key)
-        if content is None:
-            # Removes Byte Oorder Mark
-            charset = "utf-8-sig"
-            basename = self.get_basename(url)
+        # Removes Byte Oorder Mark
+        charset = "utf-8-sig"
+        basename = self.get_basename(url)
 
-            if basename is None:
-                return None
+        if basename is None:
+            return None
 
-            filename = self.get_filename(basename)
+        filename = self.get_filename(basename)
 
-            if filename is None:
-                return None
+        if filename is None:
+            return None
 
-            with codecs.open(filename, "r", charset) as fd:
-                content = fd.read()
-            # Cache this forever, as URLs will update for new files
-            caches[CACHE_NAMESPACE].set(cache_key, content, None)
+        with codecs.open(filename, "r", charset) as fd:
+            content = fd.read()
+        # Cache this forever, as URLs will update for new files
         return content
 
     def render_to_page_load_sync_html(self):
diff --git a/kolibri/deployment/default/cache.py b/kolibri/deployment/default/cache.py
index 154a10ac12..e6b9789628 100644
--- a/kolibri/deployment/default/cache.py
+++ b/kolibri/deployment/default/cache.py
@@ -40,14 +40,6 @@ default_cache = {
     "OPTIONS": {"MAX_ENTRIES": cache_options["CACHE_MAX_ENTRIES"]},
 }
 
-built_files_prefix = "built_files"
-
-built_files_cache = {
-    "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
-    # Default time out of each cache key
-    "TIMEOUT": cache_options["CACHE_TIMEOUT"],
-    "OPTIONS": {"MAX_ENTRIES": cache_options["CACHE_MAX_ENTRIES"]},
-}
 
 # Setup a special cache specifically for items that are likely to be needed
 # to be shared across processes - most frequently, things that might be needed
@@ -86,17 +78,11 @@ if cache_options["CACHE_BACKEND"] == "redis":
         },
     }
     default_cache = copy.deepcopy(base_cache)
-    default_cache["OPTIONS"]["DB"] = cache_options["CACHE_REDIS_MIN_DB"]
-    built_files_cache = copy.deepcopy(base_cache)
-    built_files_cache["OPTIONS"]["DB"] = cache_options["CACHE_REDIS_MIN_DB"] + 1
-
-built_files_cache["KEY_PREFIX"] = built_files_prefix
+    default_cache["OPTIONS"]["DB"] = cache_options["CACHE_REDIS_DB"]
 
 CACHES = {
     # Default cache
     "default": default_cache,
-    # Cache for builtfiles - frontend assets that only change on upgrade.
-    "built_files": built_files_cache,
 }
 
 if cache_options["CACHE_BACKEND"] != "redis":
diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py
index 8e8c6109f3..2cfd3e85c5 100644
--- a/kolibri/utils/cli.py
+++ b/kolibri/utils/cli.py
@@ -456,16 +456,29 @@ def configure():
     pass
 
 
+def _format_env_var(envvar, value):
+    if value.get("deprecated", False) or envvar in value.get(
+        "deprecated_envvars", tuple()
+    ):
+        return click.style(
+            "{envvar} - DEPRECATED - {description}\n\n".format(
+                envvar=envvar, description=value.get("description", "")
+            ),
+            fg="yellow",
+        )
+    return "{envvar} - {description}\n\n".format(
+        envvar=envvar, description=value.get("description", "")
+    )
+
+
 def _get_env_vars():
     """
     Generator to iterate over all environment variables
     """
-    template = "{envvar} - {description}\n\n"
-
     from kolibri.utils.env import ENVIRONMENT_VARIABLES
 
     for key, value in ENVIRONMENT_VARIABLES.items():
-        yield template.format(envvar=key, description=value.get("description", ""))
+        yield _format_env_var(key, value)
 
     from kolibri.utils.options import option_spec
 
@@ -473,9 +486,7 @@ def _get_env_vars():
         for v in value.values():
             if "envvars" in v:
                 for envvar in v["envvars"]:
-                    yield template.format(
-                        envvar=envvar, description=v.get("description", "")
-                    )
+                    yield _format_env_var(envvar, v)
 
 
 @configure.command(help="List all available environment variables to configure Kolibri")
diff --git a/kolibri/utils/main.py b/kolibri/utils/main.py
index 4733c8ec08..f6a9ad0690 100644
--- a/kolibri/utils/main.py
+++ b/kolibri/utils/main.py
@@ -261,8 +261,3 @@ def update(old_version, new_version):
 
     with open(version_file(), "w") as f:
         f.write(kolibri.__version__)
-
-    from django.core.cache import caches
-
-    cache = caches["built_files"]
-    cache.clear()
diff --git a/kolibri/utils/options.py b/kolibri/utils/options.py
index c2b3cfbe3e..7766de423e 100644
--- a/kolibri/utils/options.py
+++ b/kolibri/utils/options.py
@@ -221,13 +221,11 @@ base_option_spec = {
             "default": "localhost:6379",
             "description": "Host and port at which to connect to Redis, Redis only.",
         },
-        "CACHE_REDIS_MIN_DB": {
+        "CACHE_REDIS_DB": {
             "type": "integer",
             "default": 0,
-            "description": """
-                The starting database number for Redis.
-                This and subsequent database numbers will be used for multiple cache scenarios.
-            """,
+            "description": "The database number for Redis.",
+            "deprecated_aliases": ("CACHE_REDIS_MIN_DB",),
         },
         "CACHE_REDIS_MAX_POOL_SIZE": {
             "type": "integer",
@@ -296,6 +294,7 @@ base_option_spec = {
             "type": "boolean",
             "default": True,
             "description": "DEPRECATED - do not use this option, use the 'kolibri services' command instead.",
+            "deprecated": True,
         },
         "CHERRYPY_THREAD_POOL": {
             "type": "integer",
@@ -366,7 +365,7 @@ base_option_spec = {
         "CENTRAL_CONTENT_BASE_URL": {
             "type": "string",
             "default": "https://studio.learningequality.org",
-            "envvars": ("CENTRAL_CONTENT_DOWNLOAD_BASE_URL",),
+            "deprecated_envvars": ("CENTRAL_CONTENT_DOWNLOAD_BASE_URL",),
             "description": """
                 URL to use as the default source for content import.
                 Slightly counterintuitively this will still be displayed in the UI as 'import from Kolibri Studio'.
@@ -382,10 +381,7 @@ base_option_spec = {
         "HTTP_PORT": {
             "type": "port",
             "default": 8080,
-            "envvars": (
-                "KOLIBRI_HTTP_PORT",
-                "KOLIBRI_LISTEN_PORT",
-            ),
+            "deprecated_envvars": ("KOLIBRI_LISTEN_PORT",),
             "description": "Sets the port that Kolibri will serve on. This can be further overridden by command line arguments.",
         },
         "RUN_MODE": {
@@ -518,7 +514,16 @@ def _get_option_spec():
     envvars = set()
     for section, opts in option_spec.items():
         for optname, attrs in opts.items():
-            opt_envvars = attrs.get("envvars", tuple())
+            if "deprecated_aliases" in attrs:
+                attrs["deprecated_envvars"] = attrs.get("deprecated_envvars", tuple())
+                for alias in attrs["deprecated_aliases"]:
+                    alias_ev = "KOLIBRI_{}".format(alias)
+                    if alias_ev not in envvars:
+                        attrs["deprecated_envvars"] += (alias_ev,)
+
+            opt_envvars = attrs.get("envvars", tuple()) + attrs.get(
+                "deprecated_envvars", tuple()
+            )
             default_envvar = "KOLIBRI_{}".format(optname.upper())
             if default_envvar not in envvars:
                 envvars.add(default_envvar)
@@ -532,7 +537,7 @@ def _get_option_spec():
                     section.upper(), optname.upper()
                 )
             if default_envvar not in opt_envvars:
-                attrs["envvars"] = opt_envvars + (default_envvar,)
+                attrs["envvars"] = (default_envvar,) + opt_envvars
     return option_spec
 
 
@@ -567,6 +572,82 @@ def get_configspec():
     return ConfigObj(lines, _inspec=True)
 
 
+def _set_from_envvars(conf):
+    """
+    Set the configuration from environment variables.
+    """
+    logger = _get_logger()
+    # keep track of which options were overridden using environment variables, to support error reporting
+    using_env_vars = {}
+
+    deprecation_warning = "Option {optname} in section [{section}] being overridden by deprecated environment variable {envvar}, please update to: {envvars}"
+    # override any values from their environment variables (if set)
+    # and check for use of deprecated environment variables and options
+    for section, opts in option_spec.items():
+        for optname, attrs in opts.items():
+            for envvar in attrs.get("envvars", []):
+                if os.environ.get(envvar):
+                    deprecated_envvars = attrs.get("deprecated_envvars", tuple())
+                    if envvar in deprecated_envvars:
+                        logger.warn(
+                            deprecation_warning.format(
+                                optname=optname,
+                                section=section,
+                                envvar=envvar,
+                                envvars=", ".join(
+                                    e
+                                    for e in attrs.get("envvars", [])
+                                    if e not in deprecated_envvars
+                                ),
+                            )
+                        )
+                    else:
+                        logger.info(
+                            "Option {optname} in section [{section}] being overridden by environment variable {envvar}".format(
+                                optname=optname, section=section, envvar=envvar
+                            )
+                        )
+                    if attrs.get("deprecated", False):
+                        logger.warn(
+                            "Option {optname} in section [{section}] is deprecated, please remove it from your options.ini file".format(
+                                optname=optname, section=section
+                            )
+                        )
+                    conf[section][optname] = os.environ[envvar]
+                    using_env_vars[optname] = envvar
+                    break
+    return using_env_vars
+
+
+def _set_from_deprecated_aliases(conf):
+    """
+    Set the configuration from deprecated aliases.
+    """
+    logger = _get_logger()
+    # keep track of which options were overridden using environment variables, to support error reporting
+    using_deprecated_alias = {}
+
+    deprecation_warning = "Option {optname} in section [{section}] being set by deprecated alias {alias}, please update to: {optname}"
+    # override any values from their environment variables (if set)
+    # and check for use of deprecated environment variables and options
+    for section, opts in option_spec.items():
+        for optname, attrs in opts.items():
+            for alias in attrs.get("deprecated_aliases", tuple()):
+                if alias in conf[section]:
+                    logger.warn(
+                        deprecation_warning.format(
+                            optname=optname,
+                            section=section,
+                            alias=alias,
+                        )
+                    )
+                    conf[section][optname] = conf[section][alias]
+                    del conf[section][alias]
+                    using_deprecated_alias[optname] = alias
+                    break
+    return using_deprecated_alias
+
+
 def read_options_file(ini_filename="options.ini"):
 
     from kolibri.utils.conf import KOLIBRI_HOME
@@ -577,25 +658,26 @@ def read_options_file(ini_filename="options.ini"):
 
     conf = ConfigObj(ini_path, configspec=get_configspec())
 
+    # Check for use of deprecated options
+    for section, opts in option_spec.items():
+        for optname, attrs in opts.items():
+            if (
+                attrs.get("deprecated", False)
+                and section in conf
+                and optname in conf[section]
+            ):
+                logger.warn(
+                    "Option {optname} in section [{section}] is deprecated, please remove it from your options.ini file".format(
+                        optname=optname, section=section
+                    )
+                )
+
     # validate once up front to ensure section structure is in place
     conf.validate(_get_validator())
 
-    # keep track of which options were overridden using environment variables, to support error reporting
-    using_env_vars = {}
+    using_env_vars = _set_from_envvars(conf)
 
-    # override any values from their environment variables (if set)
-    for section, opts in option_spec.items():
-        for optname, attrs in opts.items():
-            for envvar in attrs.get("envvars", []):
-                if os.environ.get(envvar):
-                    logger.info(
-                        "Option {optname} in section [{section}] being overridden by environment variable {envvar}".format(
-                            optname=optname, section=section, envvar=envvar
-                        )
-                    )
-                    conf[section][optname] = os.environ[envvar]
-                    using_env_vars[optname] = envvar
-                    break
+    using_deprecated_alias = _set_from_deprecated_aliases(conf)
 
     validation = conf.validate(_get_validator(), preserve_errors=True)
 
@@ -609,6 +691,15 @@ def read_options_file(ini_filename="options.ini"):
                         envvar=using_env_vars[optname], error=error
                     )
                 )
+            elif optname in using_deprecated_alias:
+                logger.error(
+                    "Error processing {file} under section [{section}] for option {alias}: {error}".format(
+                        file=ini_path,
+                        section=section,
+                        alias=using_deprecated_alias[optname],
+                        error=error,
+                    )
+                )
             else:
                 logger.error(
                     "Error processing {file} under section [{section}] for option {option}: {error}".format(
@@ -699,7 +790,9 @@ def generate_empty_options_file(ini_filename="options.ini"):
             conf.comments[section] = comments
         comments = []
         for optname, attrs in opts.items():
-            if not attrs.get("skip_blank", False):
+            if not attrs.get("skip_blank", False) and not attrs.get(
+                "deprecated", False
+            ):
                 if "description" in attrs:
                     comments.extend(attrs["description"].strip().split("\n"))
                 comments.append("{} = {}".format(optname, attrs.get("default", "")))


Test patch:
diff --git a/kolibri/utils/tests/test_options.py b/kolibri/utils/tests/test_options.py
index 1704e3e90b..a791c4a47f 100644
--- a/kolibri/utils/tests/test_options.py
+++ b/kolibri/utils/tests/test_options.py
@@ -9,6 +9,7 @@ import logging
 import os
 import sys
 import tempfile
+from contextlib import contextmanager
 
 import mock
 import pytest
@@ -31,6 +32,7 @@ def log_logger(logger_instance, LEVEL, msg, args, **kwargs):
     logger_instance.__log(LEVEL, msg, args, **kwargs)
 
 
+@contextmanager
 def activate_log_logger(monkeypatch):
     """
     Activates logging everything to ``LOG_LOGGER`` with the monkeypatch pattern
@@ -38,6 +40,9 @@ def activate_log_logger(monkeypatch):
     """
     monkeypatch.setattr(logging.Logger, "__log", logging.Logger._log, raising=False)
     monkeypatch.setattr(logging.Logger, "_log", log_logger)
+    yield
+    # Use this to clear the list for Py2 compatibility
+    del LOG_LOGGER[:]
 
 
 def test_option_reading_and_precedence_rules():
@@ -84,7 +89,7 @@ def test_option_reading_and_precedence_rules():
     # when a higher precedence env var is set, it overrides the lower precedence env var
     with mock.patch.dict(
         os.environ,
-        {"KOLIBRI_HTTP_PORT": str(_HTTP_PORT_ENV), "KOLIBRI_LISTEN_PORT": "88888"},
+        {"KOLIBRI_HTTP_PORT": str(_HTTP_PORT_ENV), "KOLIBRI_LISTEN_PORT": "64000"},
     ):
         OPTIONS = options.read_options_file(ini_filename=tmp_ini_path)
         assert OPTIONS["Deployment"]["HTTP_PORT"] == _HTTP_PORT_ENV
@@ -112,40 +117,121 @@ def test_default_envvar_generation():
 
 def test_improper_settings_display_errors_and_exit(monkeypatch):
     """
-    Checks that options can be read from a dummy options.ini file, and overridden by env vars.
+    Checks invalid options log errors and exit.
     """
 
-    activate_log_logger(monkeypatch)
+    with activate_log_logger(monkeypatch):
+
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+
+        # non-numeric arguments for an integer option in the ini file cause it to bail
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n".join(["[Deployment]", "HTTP_PORT = abba"]))
+        with mock.patch.dict(
+            os.environ, {"KOLIBRI_HTTP_PORT": "", "KOLIBRI_LISTEN_PORT": ""}
+        ):
+            with pytest.raises(SystemExit):
+                options.read_options_file(ini_filename=tmp_ini_path)
+            assert 'value "abba" is of the wrong type' in LOG_LOGGER[-2][1]
+
+        # non-numeric arguments for an integer option in the env var cause it to bail, even when ini file is ok
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n".join(["[Deployment]", "HTTP_PORT = 1278"]))
+        with mock.patch.dict(
+            os.environ, {"KOLIBRI_HTTP_PORT": "baba", "KOLIBRI_LISTEN_PORT": ""}
+        ):
+            with pytest.raises(SystemExit):
+                options.read_options_file(ini_filename=tmp_ini_path)
+            assert 'value "baba" is of the wrong type' in LOG_LOGGER[-2][1]
+
+        # invalid choice for "option" type causes it to bail
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n".join(["[Database]", "DATABASE_ENGINE = penguin"]))
+        with mock.patch.dict(os.environ, {"KOLIBRI_DATABASE_ENGINE": ""}):
+            with pytest.raises(SystemExit):
+                options.read_options_file(ini_filename=tmp_ini_path)
+            assert 'value "penguin" is unacceptable' in LOG_LOGGER[-2][1]
+
+
+def test_deprecated_values_ini_file(monkeypatch):
+    """
+    Checks that deprecated options log warnings.
+    """
 
-    _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+    with activate_log_logger(monkeypatch):
 
-    # non-numeric arguments for an integer option in the ini file cause it to bail
-    with open(tmp_ini_path, "w") as f:
-        f.write("\n".join(["[Deployment]", "HTTP_PORT = abba"]))
-    with mock.patch.dict(
-        os.environ, {"KOLIBRI_HTTP_PORT": "", "KOLIBRI_LISTEN_PORT": ""}
-    ):
-        with pytest.raises(SystemExit):
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+
+        # deprecated options in the ini file log warnings
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n".join(["[Server]", "CHERRYPY_START = false"]))
+        options.read_options_file(ini_filename=tmp_ini_path)
+        assert any("deprecated" in msg[1] for msg in LOG_LOGGER)
+
+
+def test_deprecated_values_envvars(monkeypatch):
+    """
+    Checks that deprecated options log warnings.
+    """
+
+    with activate_log_logger(monkeypatch):
+
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+        # deprecated options in the envvars log warnings
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n")
+        with mock.patch.dict(os.environ, {"KOLIBRI_CHERRYPY_START": "false"}):
             options.read_options_file(ini_filename=tmp_ini_path)
-        assert 'value "abba" is of the wrong type' in LOG_LOGGER[-2][1]
+            assert any("deprecated" in msg[1] for msg in LOG_LOGGER)
 
-    # non-numeric arguments for an integer option in the env var cause it to bail, even when ini file is ok
-    with open(tmp_ini_path, "w") as f:
-        f.write("\n".join(["[Deployment]", "HTTP_PORT = 1278"]))
-    with mock.patch.dict(
-        os.environ, {"KOLIBRI_HTTP_PORT": "baba", "KOLIBRI_LISTEN_PORT": ""}
-    ):
-        with pytest.raises(SystemExit):
+
+def test_deprecated_envvars(monkeypatch):
+    """
+    Checks that deprecated options log warnings.
+    """
+
+    with activate_log_logger(monkeypatch):
+
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+        # deprecated envvars for otherwise valid options log warnings
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n")
+        with mock.patch.dict(
+            os.environ, {"KOLIBRI_LISTEN_PORT": "1234", "KOLIBRI_HTTP_PORT": ""}
+        ):
             options.read_options_file(ini_filename=tmp_ini_path)
-        assert 'value "baba" is of the wrong type' in LOG_LOGGER[-2][1]
+            assert any("deprecated" in msg[1] for msg in LOG_LOGGER)
 
-    # invalid choice for "option" type causes it to bail
-    with open(tmp_ini_path, "w") as f:
-        f.write("\n".join(["[Database]", "DATABASE_ENGINE = penguin"]))
-    with mock.patch.dict(os.environ, {"KOLIBRI_DATABASE_ENGINE": ""}):
-        with pytest.raises(SystemExit):
+
+def test_deprecated_aliases(monkeypatch):
+    """
+    Checks that deprecated options log warnings.
+    """
+
+    with activate_log_logger(monkeypatch):
+
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+        # deprecated aliases for otherwise valid options log warnings
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n".join(["[Cache]", "CACHE_REDIS_MIN_DB = 7"]))
+        options.read_options_file(ini_filename=tmp_ini_path)
+        assert any("deprecated" in msg[1] for msg in LOG_LOGGER)
+
+
+def test_deprecated_aliases_envvars(monkeypatch):
+    """
+    Checks that deprecated options log warnings.
+    """
+
+    with activate_log_logger(monkeypatch):
+
+        _, tmp_ini_path = tempfile.mkstemp(prefix="options", suffix=".ini")
+        # envvars for deprecated aliases of otherwise valid options log warnings
+        with open(tmp_ini_path, "w") as f:
+            f.write("\n")
+        with mock.patch.dict(os.environ, {"KOLIBRI_CACHE_REDIS_MIN_DB": "1234"}):
             options.read_options_file(ini_filename=tmp_ini_path)
-        assert 'value "penguin" is unacceptable' in LOG_LOGGER[-2][1]
+            assert any("deprecated" in msg[1] for msg in LOG_LOGGER)
 
 
 def test_option_writing():