diff --git a/src/ytdl_sub/utils/ffmpeg.py b/src/ytdl_sub/utils/ffmpeg.py index 94017e95..e53e405e 100644 --- a/src/ytdl_sub/utils/ffmpeg.py +++ b/src/ytdl_sub/utils/ffmpeg.py @@ -9,6 +9,7 @@ from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger +from ytdl_sub.utils.system import IS_WINDOWS logger = Logger.get(name="ffmpeg") @@ -27,7 +28,7 @@ class FFMPEG: @classmethod def _ensure_installed(cls): try: - if sys.platform.startswith("win32"): + if IS_WINDOWS: subprocess.check_output([".\\ffmpeg", "-version"]) else: subprocess.check_output(["which", "ffmpeg"]) diff --git a/src/ytdl_sub/utils/file_lock.py b/src/ytdl_sub/utils/file_lock.py index 0b18ac41..0b801e01 100644 --- a/src/ytdl_sub/utils/file_lock.py +++ b/src/ytdl_sub/utils/file_lock.py @@ -1,67 +1,62 @@ import errno import os -import sys from contextlib import contextmanager from pathlib import Path from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.logger import Logger +from ytdl_sub.utils.system import IS_WINDOWS logger = Logger.get() - -@contextmanager -def _working_directory_lock_unix(config: ConfigFile): - """ - Create and try to lock the file /tmp/working_directory_name - - Raises - ------ - ValidationException - Lock is acquired from another process running ytdl-sub in the same working directory - OSError - Other lock error occurred - """ - # pylint: disable=import-outside-toplevel - import fcntl - - working_directory_path = Path(os.getcwd()) / config.config_options.working_directory - lock_file_path = ( - Path(os.getcwd()) - / config.config_options.lock_directory - / str(working_directory_path).replace("/", "_") - ) - - lock_file = open(lock_file_path, "w", encoding="utf-8") - - try: - fcntl.lockf(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError as exc: - if exc.errno in (errno.EACCES, errno.EAGAIN): - raise ValidationException( - "Cannot run two instances of ytdl-sub " - "with the same working directory at the same time" - ) from exc - lock_file.close() - raise exc - - try: - yield - finally: - fcntl.flock(lock_file, fcntl.LOCK_UN) - lock_file.close() - - -@contextmanager -def working_directory_lock(config: ConfigFile): - if sys.platform.startswith("win32"): +if IS_WINDOWS: + @contextmanager + def working_directory_lock(config: ConfigFile): logger.info( "Working directory lock not supported in Windows. " "Ensure only one instance of ytdl-sub runs at once using working directory %s", config.config_options.working_directory, ) yield - else: - with _working_directory_lock_unix(config): +else: + import fcntl + + @contextmanager + def working_directory_lock(config: ConfigFile): + """ + Create and try to lock the file /tmp/working_directory_name + + Raises + ------ + ValidationException + Lock is acquired from another process running ytdl-sub in the same working directory + OSError + Other lock error occurred + """ + working_directory_path = Path(os.getcwd()) / config.config_options.working_directory + lock_file_path = ( + Path(os.getcwd()) + / config.config_options.lock_directory + / str(working_directory_path).replace("/", "_") + ) + + lock_file = open(lock_file_path, "w", encoding="utf-8") + + try: + fcntl.lockf(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as exc: + if exc.errno in (errno.EACCES, errno.EAGAIN): + raise ValidationException( + "Cannot run two instances of ytdl-sub " + "with the same working directory at the same time" + ) from exc + lock_file.close() + raise exc + + try: yield + finally: + fcntl.flock(lock_file, fcntl.LOCK_UN) + lock_file.close() + diff --git a/src/ytdl_sub/utils/system.py b/src/ytdl_sub/utils/system.py new file mode 100644 index 00000000..75524d3c --- /dev/null +++ b/src/ytdl_sub/utils/system.py @@ -0,0 +1,4 @@ + +import sys + +IS_WINDOWS = sys.platform.startswith("win32") diff --git a/tests/expected_download.py b/tests/expected_download.py index c2faacbf..ab53baed 100644 --- a/tests/expected_download.py +++ b/tests/expected_download.py @@ -1,5 +1,6 @@ import json import os.path +import sys from dataclasses import dataclass from pathlib import Path from typing import List @@ -9,15 +10,20 @@ from resources import REGENERATE_FIXTURES from resources import RESOURCE_PATH from ytdl_sub.utils.file_handler import get_file_md5_hash +from ytdl_sub.utils.system import IS_WINDOWS _EXPECTED_DOWNLOADS_SUMMARY_PATH = RESOURCE_PATH / "expected_downloads_summaries" def _get_files_in_directory(relative_directory: Path | str) -> List[Path]: + relative_path_part_idx = 3 # Cuts /tmp/ + if IS_WINDOWS: + relative_path_part_idx = 7 # Cuts C:\Users\\AppData\Local\Temp\ + relative_file_paths: List[Path] = [] for path in Path(relative_directory).rglob("*"): if path.is_file(): - relative_path = Path(*path.parts[3:]) + relative_path = Path(*path.parts[relative_path_part_idx:]) relative_file_paths.append(relative_path) return relative_file_paths @@ -70,7 +76,8 @@ class ExpectedDownloads: full_path = Path(relative_directory) / path assert os.path.isfile(full_path), f"Expected {path} to be a file but it is not" - if path in ignore_md5_hashes_for or path.endswith(".info.json"): + # TODO: Implement file hash for tests in Windows + if IS_WINDOWS or path in ignore_md5_hashes_for or path.endswith(".info.json"): continue md5_hash = get_file_md5_hash(full_file_path=full_path) diff --git a/tests/unit/cli/test_working_directory_lock.py b/tests/unit/cli/test_working_directory_lock.py index 98a72397..8787ac78 100644 --- a/tests/unit/cli/test_working_directory_lock.py +++ b/tests/unit/cli/test_working_directory_lock.py @@ -6,6 +6,7 @@ import pytest from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_lock import working_directory_lock +from ytdl_sub.utils.system import IS_WINDOWS @pytest.fixture @@ -16,6 +17,9 @@ def config() -> ConfigFile: def test_working_directory_lock(config: ConfigFile): + if IS_WINDOWS: + return + new_pid = os.fork() if new_pid == 0: # is child with working_directory_lock(config=config): diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index e7a5fe51..d46534bd 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -95,6 +95,9 @@ class TestLogger: assert lines == ["[ytdl-sub:name_test] info test\n", "[ytdl-sub:name_test] debug test\n"] # Ensure the file cleans up too + for handler in logger.handlers: + handler.close() + Logger.cleanup(delete_debug_file=True) assert not os.path.isfile(Logger._DEBUG_LOGGER_FILE.name) diff --git a/tests/unit/utils/test_yaml.py b/tests/unit/utils/test_yaml.py index 1f257745..6bc4753a 100644 --- a/tests/unit/utils/test_yaml.py +++ b/tests/unit/utils/test_yaml.py @@ -1,3 +1,5 @@ +import os +import re import tempfile import pytest @@ -21,10 +23,13 @@ def bad_yaml() -> str: @pytest.fixture def bad_yaml_file_path(bad_yaml) -> str: - with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp_file: + # Do not delete the file in the context manager - for Windows compatibility + with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as tmp_file: tmp_file.write(bad_yaml.encode("utf-8")) tmp_file.flush() - yield tmp_file.name + + yield tmp_file.name + os.remove(tmp_file.name) def test_load_yaml_file_not_found(): @@ -36,6 +41,6 @@ def test_load_yaml_file_not_found(): def test_load_yaml_invalid_syntax(bad_yaml_file_path): with pytest.raises( InvalidYamlException, - match=f"'{bad_yaml_file_path}' has invalid YAML, copy-paste it into a YAML checker to find the issue.", + match=re.escape(f"'{bad_yaml_file_path}' has invalid YAML, copy-paste it into a YAML checker to find the issue."), ): load_yaml(file_path=bad_yaml_file_path)