import contextlib import logging import os.path import sys from unittest.mock import patch import pytest from src.ytdl_sub import __local_version__ from src.ytdl_sub.main import main from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger, LoggerLevels @pytest.fixture def expected_uncaught_error_message(): return ( "Version %s\nPlease 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!" ) @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 @pytest.mark.parametrize("return_code", [0, 1]) def test_main_exit_code(mock_sys_exit, return_code: int): with ( mock_sys_exit(expected_exit_code=return_code), patch("src.ytdl_sub.main._main") as mock_inner_main, patch.object(Logger, "cleanup") as mock_logger_cleanup, ): mock_inner_main.return_value = return_code main() assert mock_logger_cleanup.call_count == 1 assert mock_logger_cleanup.call_args.kwargs["has_error"] == ( True if return_code != 0 else False ) def test_main_validation_error(capsys, mock_sys_exit): validation_exception = ValidationException("test exc") with ( mock_sys_exit(expected_exit_code=1), patch("src.ytdl_sub.main._main", side_effect=validation_exception), patch.object(logging.Logger, "error") as mock_logger, ): main() assert mock_logger.call_count == 1 assert mock_logger.call_args.args[0] == "test exc" 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), patch("src.ytdl_sub.main._main", side_effect=uncaught_error), patch.object(logging.Logger, "exception") as mock_exception, patch.object(logging.Logger, "error") as mock_error, ): main() assert mock_exception.call_count == 1 assert mock_exception.call_args.args[0] == "An uncaught error occurred:" assert mock_error.call_count == 1 assert mock_error.call_args.args[0] == expected_uncaught_error_message assert mock_error.call_args.args[1] == __local_version__ assert mock_error.call_args.args[2] == Logger.error_log_filename() def test_main_permission_error(capsys, mock_sys_exit, expected_uncaught_error_message): permission_error = PermissionError("test") with ( mock_sys_exit(expected_exit_code=1), patch("src.ytdl_sub.main._main", side_effect=permission_error), patch.object(logging.Logger, "error") as mock_error, ): main() assert mock_error.call_count == 1 assert mock_error.call_args.args[0] == ( "A permission error occurred:\n%s\n" "The user running ytdl-sub must have permission to this file/directory." ) assert mock_error.call_args.args[1] == "test" def test_args_after_sub_work(mock_sys_exit, tv_show_config_path): with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", ["ytdl-sub", "-c", tv_show_config_path, "sub", "--log-level", "verbose"], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub, ): main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path assert mock_sub.call_args.kwargs["subscription_matches"] == [] assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE def test_sub_match_arguments_before(mock_sys_exit, tv_show_config_path): with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", [ "ytdl-sub", "--match", "testA", "testB", "-c", tv_show_config_path, "sub", "--log-level", "verbose", ], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub, ): main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path assert mock_sub.call_args.kwargs["subscription_matches"] == ["testA", "testB"] assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE def test_sub_match_arguments_after_many(mock_sys_exit, tv_show_config_path): with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", [ "ytdl-sub", "-c", tv_show_config_path, "sub", "--log-level", "verbose", "--match", "testA", "--match", "testB", ], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub, ): main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] assert mock_sub.call_args.kwargs["config"]._name == tv_show_config_path assert mock_sub.call_args.kwargs["subscription_matches"] == ["testA", "testB"] assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE def test_no_config_works(mock_sys_exit): with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", ["ytdl-sub", "sub", "--log-level", "verbose"], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub, ): main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] assert mock_sub.call_args.kwargs["config"]._name == "default_config" assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE def test_uses_default_config_if_present(mock_sys_exit): # If a config exists in the ytdl-sub root dir, just use that and do not delete it preexisting_default_config = os.path.isfile(DEFAULT_CONFIG_FILE_NAME) if not preexisting_default_config: open(DEFAULT_CONFIG_FILE_NAME, "a").close() try: with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", ["ytdl-sub", "sub", "--log-level", "verbose"], ), patch("ytdl_sub.cli.entrypoint._download_subscriptions_from_yaml_files") as mock_sub, patch.object( ConfigFile, "from_file_path", new=lambda _: ConfigFile(name="test default", value={}), ), ): main() assert mock_sub.call_count == 1 assert mock_sub.call_args.kwargs["subscription_paths"] == ["subscriptions.yaml"] assert mock_sub.call_args.kwargs["config"]._name == "test default" assert Logger._LOGGER_LEVEL == LoggerLevels.VERBOSE finally: if not preexisting_default_config: FileHandler.delete(DEFAULT_CONFIG_FILE_NAME) def test_no_positional_arg_command(mock_sys_exit, tv_show_config_path): with ( mock_sys_exit(expected_exit_code=1), patch.object( sys, "argv", ["ytdl-sub", "-c", tv_show_config_path, "--log-level", "verbose"], ), patch.object(logging.Logger, "error") as mock_error, ): main() assert mock_error.call_count == 1 assert mock_error.call_args.args[0] == ( "Must provide one of the commands: sub, dl, view, cli-to-sub" ) def test_bad_config_path(mock_sys_exit): with ( mock_sys_exit(expected_exit_code=1), patch.object( sys, "argv", ["ytdl-sub", "-c", "does_not_exist.yaml", "sub", "--log-level", "verbose"], ), patch.object(logging.Logger, "error") as mock_error, ): main() assert mock_error.call_count == 1 assert mock_error.call_args.args[0] == ( "The config file 'does_not_exist.yaml' could not be found. " "Did you set --config correctly?" ) def test_cli_to_sub(mock_sys_exit, capsys): with ( mock_sys_exit(expected_exit_code=0), patch.object( sys, "argv", ["ytdl-sub", "cli-to-sub", "--mark-watched", "--mtime"], ), ): main() captured = capsys.readouterr() assert captured.out == ("ytdl_options:\n mark_watched: true\n updatetime: true\n\n")