From 8a95ca9ff027ca1c1b4349ed017340c562c72729 Mon Sep 17 00:00:00 2001 From: jbannon Date: Tue, 7 Jun 2022 07:13:55 +0000 Subject: [PATCH] main fixed and tested --- Makefile | 12 +++++-- src/ytdl_sub/main.py | 8 +++-- src/ytdl_sub/utils/logger.py | 48 ++++++++++++++++--------- tests/unit/main/__init__.py | 0 tests/unit/main/test_main.py | 63 +++++++++++++++++++++++++++++++++ tests/unit/utils/test_logger.py | 6 ++++ 6 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 tests/unit/main/__init__.py create mode 100644 tests/unit/main/test_main.py diff --git a/Makefile b/Makefile index 8d53d610..e650882e 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,18 @@ -wheel: +wheel: clean python setup.py bdist_wheel docker: wheel cp dist/*.whl docker/root/ sudo docker build --no-cache -t ytdl-sub:0.1 docker/ docs: sphinx-build -a -b html docs docs/_html +clean: + rm -rf \ + .pytest_cache/ \ + build/ \ + dist/ \ + src/ytdl_sub.egg-info/ \ + .coverage \ + coverage.xml -.PHONY: wheel docker docs +.PHONY: wheel docker docs clean diff --git a/src/ytdl_sub/main.py b/src/ytdl_sub/main.py index 695e6ac0..e1fa0e72 100644 --- a/src/ytdl_sub/main.py +++ b/src/ytdl_sub/main.py @@ -83,6 +83,9 @@ def _main(): _download_subscription_from_cli(config=config, extra_args=extra_args) logger.info("Download complete!") + # Ran successfully, so we can delete the debug file + Logger.cleanup(delete_debug_file=True) + def main(): """ @@ -96,9 +99,10 @@ def main(): except Exception: # pylint: disable=broad-except logger.exception("An uncaught error occurred:") logger.error( - "Please copy and paste the stacktrace above and make a Github " + "Please upload the error log file '%s' and make a Github " "issue at https://github.com/jmbannon/ytdl-sub/issues with your config and " - "command/subscription yaml file to reproduce. Thanks for trying ytdl-sub!" + "command/subscription yaml file to reproduce. Thanks for trying ytdl-sub!", + Logger.debug_log_filename(), ) sys.exit(1) diff --git a/src/ytdl_sub/utils/logger.py b/src/ytdl_sub/utils/logger.py index 9919c5e7..f3208034 100644 --- a/src/ytdl_sub/utils/logger.py +++ b/src/ytdl_sub/utils/logger.py @@ -21,7 +21,7 @@ class LoggerLevels: Custom log levels """ - QUIET = LoggerLevel(name="quiet", level=0, logging_level=logging.NOTSET) # No logs whatsoever + QUIET = LoggerLevel(name="quiet", level=0, logging_level=logging.WARNING) # Only warnings INFO = LoggerLevel(name="info", level=10, logging_level=logging.INFO) # ytdl-sub info logs VERBOSE = LoggerLevel(name="verbose", level=20, logging_level=logging.INFO) # ytdl-sub + yt-dlp DEBUG = LoggerLevel( @@ -65,12 +65,37 @@ class LoggerLevels: return [logger_level.name for logger_level in cls.all()] +class StreamToLogger(io.StringIO): + def __init__(self, logger: logging.Logger, *args, **kwargs): + super().__init__(*args, **kwargs) + self._logger = logger + + def write(self, __s: str) -> int: + """ + Writes to the logger and stream + """ + self._logger.info(__s.removesuffix("\n")) + return super().write(__s) + + class Logger: # The level set via CLI arguments _LOGGER_LEVEL: LoggerLevel = LoggerLevels.DEBUG - _DEBUG_LOGGER_FILE = None + # Ignore 'using with' warning since this will be cleaned up later + # pylint: disable=R1732 + _DEBUG_LOGGER_FILE = tempfile.NamedTemporaryFile(prefix="ytdl-sub.", delete=False) + # pylint: enable=R1732 + + @classmethod + def debug_log_filename(cls) -> str: + """ + Returns + ------- + File name of the debug log file + """ + return cls._DEBUG_LOGGER_FILE.name @classmethod def set_log_level(cls, log_level_name: str): @@ -105,13 +130,7 @@ class Logger: @classmethod def _get_debug_file_handler(cls) -> logging.FileHandler: - if cls._DEBUG_LOGGER_FILE is None: - # Ignore 'using with' warning since this must be cleaned up later - # pylint: disable=R1732 - cls._DEBUG_LOGGER_FILE = tempfile.NamedTemporaryFile(prefix="ytdl-sub.", delete=False) - # pylint: enable=R1732 - - handler = logging.FileHandler(filename=cls._DEBUG_LOGGER_FILE.name, encoding="utf-8") + handler = logging.FileHandler(filename=cls.debug_log_filename(), encoding="utf-8") handler.setLevel(logging.DEBUG) handler.setFormatter(cls._get_formatter()) return handler @@ -158,20 +177,17 @@ class Logger: ---------- name Optional. Name of the logger which is included in the prefix like [ytdl-sub:]. - If None, the prefix is just [ytdl-sub] + If None, the prefix is just [ytdl-sub]mak """ logger = cls._get( name=name, stdout=cls._LOGGER_LEVEL.level >= LoggerLevels.VERBOSE.level, debug_file=True ) - with io.StringIO() as redirect_stream: + with StreamToLogger(logger=logger) as redirect_stream: with contextlib.redirect_stdout(new_target=redirect_stream): with contextlib.redirect_stderr(new_target=redirect_stream): yield - redirect_stream.flush() - logger.info(redirect_stream.getvalue()) - @classmethod def cleanup(cls, delete_debug_file: bool = True): """ @@ -184,5 +200,5 @@ class Logger: """ cls._DEBUG_LOGGER_FILE.close() - if delete_debug_file and os.path.isfile(cls._DEBUG_LOGGER_FILE.name): - os.remove(cls._DEBUG_LOGGER_FILE.name) + if delete_debug_file and os.path.isfile(cls.debug_log_filename()): + os.remove(cls.debug_log_filename()) diff --git a/tests/unit/main/__init__.py b/tests/unit/main/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/main/test_main.py b/tests/unit/main/test_main.py new file mode 100644 index 00000000..8a71597b --- /dev/null +++ b/tests/unit/main/test_main.py @@ -0,0 +1,63 @@ +import contextlib +import sys +from unittest.mock import patch + +import pytest + +from src.ytdl_sub.main import main +from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.logger import Logger + + +@pytest.fixture +def expected_uncaught_error_message(): + return ( + f"Please upload the error log file '%s' and make a " + f"Github issue at https://github.com/jmbannon/ytdl-sub/issues with your config and " + f"command/subscription yaml file to reproduce. Thanks for trying ytdl-sub!" + ) + + +@pytest.fixture +def mock_sys_exit(): + @contextlib.contextmanager + def _mock_sys_exit(expected_exit_code: int): + with patch.object(sys, "exit") as mock_exit: + yield mock_exit + + assert mock_exit.called + assert mock_exit.call_args_list[0].args[0] == expected_exit_code + + return _mock_sys_exit + + +def test_main_success(mock_sys_exit): + with mock_sys_exit(expected_exit_code=0): + with patch("src.ytdl_sub.main._main"): + main() + + +def test_main_validation_error(capsys, mock_sys_exit): + validation_exception = ValidationException("test") + with mock_sys_exit(expected_exit_code=1): + with patch("src.ytdl_sub.main._main", side_effect=validation_exception): + with patch("src.ytdl_sub.main.logger") as mock_logger: + main() + + assert mock_logger.error.call_count == 1 + assert mock_logger.error.call_args.args[0] == validation_exception + + +def test_main_uncaught_error(capsys, mock_sys_exit, expected_uncaught_error_message): + uncaught_error = ValueError("test") + with mock_sys_exit(expected_exit_code=1): + with patch("src.ytdl_sub.main._main", side_effect=uncaught_error): + with patch("src.ytdl_sub.main.logger") as mock_logger: + main() + + assert mock_logger.exception.call_count == 1 + assert mock_logger.exception.call_args.args[0] == "An uncaught error occurred:" + + assert mock_logger.error.call_count == 1 + assert mock_logger.error.call_args.args[0] == expected_uncaught_error_message + assert mock_logger.error.call_args.args[1] == Logger.debug_log_filename() diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index bc06c8cc..ccd33a3d 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -1,3 +1,5 @@ +import os.path + import pytest from ytdl_sub.utils.logger import Logger @@ -75,6 +77,10 @@ class TestLogger: assert lines == ["[ytdl-sub:name_test] info test\n", "[ytdl-sub:name_test] debug test\n"] + # Ensure the file cleans up too + Logger.cleanup(delete_debug_file=True) + assert not os.path.isfile(Logger._DEBUG_LOGGER_FILE.name) + @pytest.mark.parametrize( "log_level, expected_stdout", [