From 88d3eab320ea8bd78228cb7c7b71cc48b2f3e66e Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 15 Jul 2024 18:19:37 -0700 Subject: [PATCH 01/11] Add docstrings and type annotations to `log` module Add test coverage for log rotation Outline test coverage for added log module functions Relates to #2 #239 #233 #250 #253 --- ovos_utils/log.py | 26 +++++++--- test/unittests/test_log.py | 101 +++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 70cbe7ca..374c2512 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -205,11 +205,16 @@ def _monitor_log_level(): _monitor_log_level.config_hash = None -def init_service_logger(service_name): +def init_service_logger(service_name: str): + """ + Initialize `LOG` for the specified service + @param service_name: Name of service to configure `LOG` for + """ _logs_conf = get_logs_config(service_name) - _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True, indent=2)) + _monitor_log_level.config_hash = hash(json.dumps(_logs_conf, sort_keys=True, + indent=2)) LOG.name = service_name - LOG.init(_logs_conf) # setup the LOG instance + LOG.init(_logs_conf) # set up the LOG instance try: from ovos_config import Configuration Configuration.set_config_watcher(_monitor_log_level) @@ -217,7 +222,14 @@ def init_service_logger(service_name): LOG.warning("Can not monitor config LOG level changes") -def get_logs_config(service_name=None, _cfg=None) -> dict: +def get_logs_config(service_name: Optional[str] = None, + _cfg: Optional[dict] = None) -> dict: + """ + Get logging configuration for the specified service + @param service_name: Name of service to get logging configuration for + @param _cfg: Configuration to parse + @return: dict logging configuration for the specified service + """ if _cfg is None: try: from ovos_config import Configuration @@ -342,9 +354,9 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ xdg_base = os.environ.get("OVOS_CONFIG_BASE_FOLDER", "mycroft") return os.path.join(xdg_state_home(), xdg_base) - config = Configuration().get("logging", dict()).get("logs", dict()) + config = get_logs_config(service_name=service) # service specific config or default config location - path = config.get(service, {}).get("path") or config.get("path") + path = config.get("path") # default xdg location if not path: path = os.path.join(xdg_state_home(), get_xdg_base()) @@ -375,7 +387,7 @@ def get_available_logs(directories: Optional[List[str]] = None) -> List[str]: directories: (optional) list of directories to check for service Returns: - list of log files + list of log file basenames (i.e. "audio", "skills") """ directories = directories or get_log_paths() return [Path(f).stem for path in directories diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 751f96ae..79a3064c 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -60,9 +60,61 @@ def test_log(self): self.assertEqual(len(lines), 1) self.assertTrue(lines[0].endswith("This will print\n")) - def test_init_service_logger(self): - from ovos_utils.log import init_service_logger - # TODO + # Init with backup + test_config['max_bytes'] = 2 + test_config['backup_count'] = 1 + test_config['level'] = 'INFO' + LOG.init(test_config) + LOG.name = "rotate" + LOG.info("first") + LOG.info("second") + LOG.debug("third") + log_1 = join(LOG.base_path, f"{LOG.name}.log.1") + log = join(LOG.base_path, f"{LOG.name}.log") + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("first\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("second\n")) + + LOG.info("fourth") + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("second\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("fourth\n")) + + @patch("ovos_utils.log.get_logs_config") + @patch("ovos_config.Configuration.set_config_watcher") + def test_init_service_logger(self, set_config_watcher, log_config): + from ovos_utils.log import init_service_logger, LOG + + # Test log init with default config + log_config.return_value = dict() + LOG.level = "ERROR" + init_service_logger("default") + from ovos_utils.log import LOG + set_config_watcher.assert_called_once() + self.assertEqual(LOG.name, "default") + self.assertEqual(LOG.level, "ERROR") + + # Test log init with config + set_config_watcher.reset_mock() + log_config.return_value = {"path": self.test_dir, + "level": "DEBUG"} + init_service_logger("configured") + from ovos_utils.log import LOG + set_config_watcher.assert_called_once() + self.assertEqual(LOG.name, "configured") + self.assertEqual(LOG.level, "DEBUG") + LOG.debug("This will print") + self.assertTrue(isfile(join(self.test_dir, "configured.log"))) @patch("ovos_utils.log.LOG.create_logger") def test_log_deprecation(self, create_logger): @@ -115,3 +167,46 @@ def _deprecated_function(test_arg): log_msg = log_warning.call_args[0][0] self.assertIn('version=1.0.0', log_msg, log_msg) self.assertIn('test deprecation', log_msg, log_msg) + + def test_monitor_log_level(self): + from ovos_utils.log import _monitor_log_level + # TODO + + def test_get_logs_config(self): + from ovos_utils.log import get_logs_config + + # Test original config with `logs` section and no `logging` section + + # Test `logging.logs` config with no service config + + # Test `logging.logs` config with `logging.` overrides + + # Test `logs` config with `logging.` overrides + + # Test `logging.` config with no `logs` or `logging.logs` + + def test_get_log_path(self): + from ovos_utils.log import get_log_path + + # Test with multiple populated directories + + # Test with specified empty directory + + # Test path from configuration + + @patch('ovos_utils.log.get_log_path') + def test_get_log_paths(self, get_log_path): + from ovos_utils.log import get_log_paths + + # Test services with different configured paths + + @patch('ovos_utils.log.get_log_paths') + def test_get_available_logs(self, get_log_paths): + from ovos_utils.log import get_available_logs + + # Test with specified directories containing logs and other files + + # Test with no log directories + self.assertEqual(get_available_logs([dirname(__file__)]), []) + get_log_paths.return_value = [] + self.assertEqual(get_available_logs(), []) From 879010b328d90c8f47068611535cf8b17e2f47cb Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 15 Jul 2024 18:22:54 -0700 Subject: [PATCH 02/11] Update logs to resolve order-related failure --- test/unittests/test_log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 79a3064c..9ae8c3e5 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -17,6 +17,7 @@ def tearDownClass(cls) -> None: def test_log(self): import ovos_utils.log + importlib.reload(ovos_utils.log) from ovos_utils.log import LOG # Default log config self.assertEqual(LOG.base_path, "stdout") From bd275932124006ccb6fbb620e9b75dd28eaf87d6 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 17:20:28 -0700 Subject: [PATCH 03/11] Add test coverage for `get_logs_config` Refactor variable in `get_logs_config` for clarity Refactor `get_logs_config` for more predictable handling of an empty service name --- ovos_utils/log.py | 15 +++++++----- test/unittests/test_log.py | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 374c2512..344b7229 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -239,20 +239,23 @@ def get_logs_config(service_name: Optional[str] = None, _cfg = {} # First try and get the "logging" section - log_config = _cfg.get("logging") + logging_conf = _cfg.get("logging") # For compatibility we try to get the "logs" from the root level # and default to empty which is used in case there is no logging # section _logs_conf = _cfg.get("logs") or {} - if log_config: # We found a logging section + if logging_conf: # We found a logging section # if "logs" is defined in "logging" use that as the default # where per-service "logs" are not defined - _logs_conf = log_config.get("logs") or _logs_conf + _logs_conf = logging_conf.get("logs") or _logs_conf # Now get our config by service name if service_name: - _cfg = log_config.get(service_name) or log_config - # and if "logs" is redefined in "logging." use that - _logs_conf = _cfg.get("logs") or _logs_conf + _cfg = logging_conf.get(service_name) or logging_conf + else: + # No service name specified, use `logging` configuration + _cfg = logging_conf + # and if "logs" is redefined in "logging." use that + _logs_conf = _cfg.get("logs") or _logs_conf # Grab the log level from whatever section we found, defaulting to INFO _log_level = _cfg.get("log_level", "INFO") _logs_conf["level"] = _log_level diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 9ae8c3e5..3f72641f 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -72,6 +72,8 @@ def test_log(self): LOG.debug("third") log_1 = join(LOG.base_path, f"{LOG.name}.log.1") log = join(LOG.base_path, f"{LOG.name}.log") + + # Log rotated once with open(log_1) as f: lines = f.readlines() self.assertEqual(len(lines), 1) @@ -82,6 +84,7 @@ def test_log(self): self.assertTrue(lines[0].endswith("second\n")) LOG.info("fourth") + # Log rotated again with open(log_1) as f: lines = f.readlines() self.assertEqual(len(lines), 1) @@ -91,6 +94,18 @@ def test_log(self): self.assertEqual(len(lines), 1) self.assertTrue(lines[0].endswith("fourth\n")) + # Multiple log rotations within a short period of time + for i in range(100): + LOG.info(str(i)) + with open(log_1) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("98\n")) + with open(log) as f: + lines = f.readlines() + self.assertEqual(len(lines), 1) + self.assertTrue(lines[0].endswith("99\n")) + @patch("ovos_utils.log.get_logs_config") @patch("ovos_config.Configuration.set_config_watcher") def test_init_service_logger(self, set_config_watcher, log_config): @@ -175,16 +190,48 @@ def test_monitor_log_level(self): def test_get_logs_config(self): from ovos_utils.log import get_logs_config + valid_config = {"level": "DEBUG", + "path": self.test_dir, + "max_bytes": 1000, + "backup_count": 2, + "diagnostic": False} + valid_config_2 = {"max_bytes": 100000, + "diagnostic": True} + logs_config = {"path": self.test_dir, + "max_bytes": 1000, + "backup_count": 2, + "diagnostic": False} + legacy_config = {"log_level": "DEBUG", + "logs": logs_config} + + logging_config = {"logging": {"log_level": "DEBUG", + "logs": logs_config, + "test_service": {"log_level": "WARNING", + "logs": valid_config_2} + } + } # Test original config with `logs` section and no `logging` section + self.assertEqual(get_logs_config("", legacy_config), valid_config) # Test `logging.logs` config with no service config + self.assertEqual(get_logs_config("service", logging_config), valid_config) # Test `logging.logs` config with `logging.` overrides + expected_config = {**valid_config_2, **{"level": "WARNING"}} + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) # Test `logs` config with `logging.` overrides + logging_config["logs"] = logging_config["logging"].pop("logs") + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) # Test `logging.` config with no `logs` or `logging.logs` + logging_config["logging"].pop("log_level") + logging_config.pop("logs") + self.assertEqual(get_logs_config("test_service", logging_config), + expected_config) def test_get_log_path(self): from ovos_utils.log import get_log_path From f30225200019a08e4b78ed23f98277b90b519ad0 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 17:27:48 -0700 Subject: [PATCH 04/11] Add test coverage for `get_log_path` --- test/unittests/test_log.py | 13 ++++++++++--- test/unittests/test_logs/real.log | 0 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 test/unittests/test_logs/real.log diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 3f72641f..25465f0a 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -233,14 +233,21 @@ def test_get_logs_config(self): self.assertEqual(get_logs_config("test_service", logging_config), expected_config) - def test_get_log_path(self): + @patch("ovos_utils.log.get_logs_config") + def test_get_log_path(self, get_config): from ovos_utils.log import get_log_path - # Test with multiple populated directories + real_log_path = join(dirname(__file__), "test_logs") + test_paths = [self.test_dir, dirname(__file__), real_log_path] - # Test with specified empty directory + # Test with multiple populated directories + self.assertEqual(get_log_path("real", test_paths), real_log_path) + self.assertIsNone(get_log_path("fake", test_paths)) # Test path from configuration + get_config.return_value = {"path": self.test_dir} + self.assertEqual(get_log_path("test"), self.test_dir) + get_config.assert_called_once_with(service_name="test") @patch('ovos_utils.log.get_log_path') def test_get_log_paths(self, get_log_path): diff --git a/test/unittests/test_logs/real.log b/test/unittests/test_logs/real.log new file mode 100644 index 00000000..e69de29b From 9d20e91e89bb68b9387b1a979f5e996396ea05b6 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 17:41:45 -0700 Subject: [PATCH 05/11] Add test coverage for `get_log_paths` Update `get_log_paths` to check all log names with `-` normalized to `_` (appeared to be the original intent) Add `enclosure` and `admin` service logs used by Neon and legacy Mycroft/OVOS setups --- ovos_utils/log.py | 7 +++++-- test/unittests/test_log.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 344b7229..115f5244 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -29,6 +29,8 @@ "ovos", "phal", "phal-admin", + "enclosure", + "admin", "hivemind", "hivemind-voice-sat"} @@ -341,6 +343,7 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ Returns: path to log directory for service + (returned path may be `None` if `directories` is specified) """ if directories: for directory in directories: @@ -376,8 +379,8 @@ def get_log_paths() -> Set[str]: set of paths to log directories """ paths = set() - ALL_SERVICES.union({s.replace("-", "_") for s in ALL_SERVICES}) - for service in ALL_SERVICES: + svc_names = ALL_SERVICES.union({s.replace("-", "_") for s in ALL_SERVICES}) + for service in svc_names: paths.add(get_log_path(service)) return paths diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 25465f0a..7852a2b6 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -251,9 +251,25 @@ def test_get_log_path(self, get_config): @patch('ovos_utils.log.get_log_path') def test_get_log_paths(self, get_log_path): - from ovos_utils.log import get_log_paths + from ovos_utils.log import get_log_paths, ALL_SERVICES + + def mock_get_path_different(service) -> str: + return service + + def mock_get_path_same(service) -> str: + return "log_path" # Test services with different configured paths + get_log_path.side_effect = mock_get_path_different + paths = get_log_paths() + for svc in ALL_SERVICES: + self.assertIn(svc, paths) + if '-' in svc: + self.assertIn(svc.replace('-', '_'), paths) + + # Test services with same path + get_log_path.side_effect = mock_get_path_same + self.assertEqual(get_log_paths(), {"log_path"}) @patch('ovos_utils.log.get_log_paths') def test_get_available_logs(self, get_log_paths): From 7b4252adc29e54f62c5c284afdb7db55b93f2765 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 17:44:00 -0700 Subject: [PATCH 06/11] Add test coverage for `get_available_logs` --- test/unittests/test_log.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 7852a2b6..54fb03b5 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -276,6 +276,9 @@ def test_get_available_logs(self, get_log_paths): from ovos_utils.log import get_available_logs # Test with specified directories containing logs and other files + real_log_path = join(dirname(__file__), "test_logs") + get_log_paths.return_value = [dirname(__file__), real_log_path] + self.assertEqual(get_available_logs(), ["real"]) # Test with no log directories self.assertEqual(get_available_logs([dirname(__file__)]), []) From bbe9fc83dbcce8d1e3d986b6e4e9f7ea8135e5e1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 18:21:09 -0700 Subject: [PATCH 07/11] Add test coverage for `_monitor_log_level` --- test/unittests/test_log.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 54fb03b5..2d51a947 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -2,6 +2,7 @@ import shutil import unittest import importlib +from copy import deepcopy from os.path import join, dirname, isdir, isfile from unittest.mock import patch, Mock @@ -184,9 +185,29 @@ def _deprecated_function(test_arg): self.assertIn('version=1.0.0', log_msg, log_msg) self.assertIn('test deprecation', log_msg, log_msg) - def test_monitor_log_level(self): + @patch("ovos_utils.log.get_logs_config") + @patch("ovos_utils.log.LOG") + def test_monitor_log_level(self, log, get_config): from ovos_utils.log import _monitor_log_level - # TODO + + log.name = "TEST" + get_config.return_value = {"changed": False} + + _monitor_log_level() + get_config.assert_called_once_with("TEST") + log.init.assert_called_once_with(get_config.return_value) + log.info.assert_called_once() + + # Callback with no change + _monitor_log_level() + log.init.assert_called_once_with(get_config.return_value) + log.info.assert_called_once() + + # Callback with change + get_config.return_value["changed"] = True + _monitor_log_level() + self.assertEqual(log.init.call_count, 2) + log.init.assert_called_with(get_config.return_value) def test_get_logs_config(self): from ovos_utils.log import get_logs_config From 2c96bdb861ce26e3900a9bf8988011598e1ef50f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 17 Jul 2024 18:23:24 -0700 Subject: [PATCH 08/11] Add test of `get_config` call count --- test/unittests/test_log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index 2d51a947..fcf6c3dc 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -200,12 +200,14 @@ def test_monitor_log_level(self, log, get_config): # Callback with no change _monitor_log_level() + self.assertEqual(get_config.call_count, 2) log.init.assert_called_once_with(get_config.return_value) log.info.assert_called_once() # Callback with change get_config.return_value["changed"] = True _monitor_log_level() + self.assertEqual(get_config.call_count, 3) self.assertEqual(log.init.call_count, 2) log.init.assert_called_with(get_config.return_value) From 89db8948c6ce13f0588efeea8e7b7f9161c62447 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jul 2024 09:55:14 -0700 Subject: [PATCH 09/11] Update `get_log_paths` to reference config directly instead of test some well-known values Update unit test for `get_log_paths` --- ovos_utils/log.py | 19 +++++++++++++++---- test/unittests/test_log.py | 31 ++++++++++++++----------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 115f5244..509dae57 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -370,7 +370,7 @@ def get_log_path(service: str, directories: Optional[List[str]] = None) \ return path -def get_log_paths() -> Set[str]: +def get_log_paths(config: Optional[dict] = None) -> Set[str]: """ Get all log paths for all service logs Different services may have different log paths @@ -379,9 +379,20 @@ def get_log_paths() -> Set[str]: set of paths to log directories """ paths = set() - svc_names = ALL_SERVICES.union({s.replace("-", "_") for s in ALL_SERVICES}) - for service in svc_names: - paths.add(get_log_path(service)) + if not config: + try: + from ovos_config import Configuration + config = Configuration() + except ImportError: + LOG.warning("ovos_config not available. Falling back to defaults") + config = dict() + + for name, service_config in config.get("logging", {}).items(): + if not isinstance(service_config, dict) or name == "logs": + continue + if service_config.get("path"): + paths.add(service_config.get("path")) + paths.add(get_log_path("")) return paths diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index fcf6c3dc..fb3f6417 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -272,27 +272,24 @@ def test_get_log_path(self, get_config): self.assertEqual(get_log_path("test"), self.test_dir) get_config.assert_called_once_with(service_name="test") - @patch('ovos_utils.log.get_log_path') - def test_get_log_paths(self, get_log_path): - from ovos_utils.log import get_log_paths, ALL_SERVICES + @patch('ovos_config.Configuration') + def test_get_log_paths(self, config): + from ovos_utils.log import get_log_paths - def mock_get_path_different(service) -> str: - return service + config_no_modules = {"logging": {"logs": {"path": "default_path"}}} - def mock_get_path_same(service) -> str: - return "log_path" + # Test default config path from Configuration (no module overrides) + config.return_value = config_no_modules + self.assertEqual(get_log_paths(), {"default_path"}) # Test services with different configured paths - get_log_path.side_effect = mock_get_path_different - paths = get_log_paths() - for svc in ALL_SERVICES: - self.assertIn(svc, paths) - if '-' in svc: - self.assertIn(svc.replace('-', '_'), paths) - - # Test services with same path - get_log_path.side_effect = mock_get_path_same - self.assertEqual(get_log_paths(), {"log_path"}) + config_multi_modules = {"logging": {"logs": {"path": "default_path"}, + "module_1": {"path": "path_1"}, + "module_2": {"path": "path_2"}, + "module_3": {"path": "path_1"}}} + self.assertEqual(get_log_paths(config_multi_modules), + {"default_path", "path_1", "path_2"}) + @patch('ovos_utils.log.get_log_paths') def test_get_available_logs(self, get_log_paths): From a7672b907abea68e7dbd98eca5137ac989e8ec6f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jul 2024 13:01:52 -0700 Subject: [PATCH 10/11] Remove unused `ALL_SERVICES` variable --- ovos_utils/log.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ovos_utils/log.py b/ovos_utils/log.py index 509dae57..6f4c69a0 100644 --- a/ovos_utils/log.py +++ b/ovos_utils/log.py @@ -21,19 +21,6 @@ from pathlib import Path from typing import Optional, List, Set -ALL_SERVICES = {"bus", - "audio", - "skills", - "voice", - "gui", - "ovos", - "phal", - "phal-admin", - "enclosure", - "admin", - "hivemind", - "hivemind-voice-sat"} - class LOG: """ From 6be5c6176bcf813878539e7ba68e6004b7dbfd1f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 18 Jul 2024 13:02:37 -0700 Subject: [PATCH 11/11] Remove unused import --- test/unittests/test_log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unittests/test_log.py b/test/unittests/test_log.py index fb3f6417..f816cf1b 100644 --- a/test/unittests/test_log.py +++ b/test/unittests/test_log.py @@ -2,7 +2,6 @@ import shutil import unittest import importlib -from copy import deepcopy from os.path import join, dirname, isdir, isfile from unittest.mock import patch, Mock