diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4991aba..ebfc5ca 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -38,4 +38,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - pytest tests/*.py tests/*/*.py + pytest tests/ diff --git a/scripts/gdm.py b/scripts/gdm.py index a5c23cf..d08ab15 100644 --- a/scripts/gdm.py +++ b/scripts/gdm.py @@ -7,6 +7,7 @@ from .utils import remove_properties, remove_keywords from . import config from .utils.alternatives_updater import AlternativesUpdater from scripts.utils.logger.console import Console, Color, Format +from .utils.command_runner.subprocess_command_runner import SubprocessCommandRunner from .utils.files_labeler import FilesLabeler from .utils.gresource import GresourceBackupNotFoundError from .utils.gresource.gresource import Gresource @@ -41,7 +42,8 @@ class GlobalTheme: self.__gresource_file = os.path.join(self.destination_folder, self.destination_file) self.__gresource_temp_folder = os.path.join(self.temp_folder, config.extracted_gdm_folder) - self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder, logger_factory=Console()) + self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder, + logger_factory=Console(), runner=SubprocessCommandRunner()) def prepare(self): if self.__is_installed(): diff --git a/scripts/utils/command_runner/__init__.py b/scripts/utils/command_runner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/utils/command_runner/command_runner.py b/scripts/utils/command_runner/command_runner.py new file mode 100644 index 0000000..abbbca0 --- /dev/null +++ b/scripts/utils/command_runner/command_runner.py @@ -0,0 +1,14 @@ +import subprocess +from abc import ABC, abstractmethod + + +class CommandRunner(ABC): + @abstractmethod + def run(self, command: list[str], **kwargs) -> subprocess.CompletedProcess: + """ + Run a command in the shell and return the output. + :param command: Command to run. + :param kwargs: Additional arguments for the command. + :return: Output of the command. + """ + pass \ No newline at end of file diff --git a/scripts/utils/command_runner/subprocess_command_runner.py b/scripts/utils/command_runner/subprocess_command_runner.py new file mode 100644 index 0000000..0f9732e --- /dev/null +++ b/scripts/utils/command_runner/subprocess_command_runner.py @@ -0,0 +1,8 @@ +import subprocess + +from scripts.utils.command_runner.command_runner import CommandRunner + + +class SubprocessCommandRunner(CommandRunner): + def run(self, command: list[str], **kwargs) -> subprocess.CompletedProcess: + return subprocess.run(command, **kwargs) \ No newline at end of file diff --git a/scripts/utils/gresource/gresource.py b/scripts/utils/gresource/gresource.py index 31295bb..3911c91 100644 --- a/scripts/utils/gresource/gresource.py +++ b/scripts/utils/gresource/gresource.py @@ -1,16 +1,20 @@ import os +from scripts.utils.command_runner.command_runner import CommandRunner from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager -from scripts.utils.gresource.gresource_complier import GresourceCompiler +from scripts.utils.gresource.gresource_compiler import GresourceCompiler from scripts.utils.gresource.gresource_extractor import GresourceExtractor from scripts.utils.gresource.gresource_mover import GresourceMover from scripts.utils.logger.logger import LoggerFactory class Gresource: - """Orchestrator for gresource files. Manages the extraction, compilation, and backup of gresource files.""" + """Orchestrator for gresource files.""" - def __init__(self, gresource_file: str, temp_folder: str, destination: str, logger_factory: LoggerFactory): + def __init__( + self, gresource_file: str, temp_folder: str, destination: str, + logger_factory: LoggerFactory, runner: CommandRunner + ): """ :param gresource_file: The name of the gresource file to be processed. :param temp_folder: The temporary folder where resources will be extracted. @@ -19,28 +23,39 @@ class Gresource: self.gresource_file = gresource_file self.temp_folder = temp_folder self.destination = destination + self.logger_factory = logger_factory + self.runner = runner self._temp_gresource = os.path.join(temp_folder, gresource_file) self._destination_gresource = os.path.join(destination, gresource_file) self._active_source_gresource = self._destination_gresource - self._backuper = GresourceBackuperManager(self._destination_gresource, logger_factory=self.logger_factory) + self._backuper = GresourceBackuperManager(self._destination_gresource, + logger_factory=self.logger_factory) def use_backup_gresource(self): self._active_source_gresource = self._backuper.get_backup() + return self._active_source_gresource def extract(self): - GresourceExtractor(self._active_source_gresource, self.temp_folder, logger_factory=self.logger_factory).extract() + extractor = GresourceExtractor(self._active_source_gresource, self.temp_folder, + logger_factory=self.logger_factory, runner=self.runner) + extractor.extract() def compile(self): - GresourceCompiler(self.temp_folder, self._temp_gresource, logger_factory=self.logger_factory).compile() + compiler = GresourceCompiler(self.temp_folder, self._temp_gresource, + logger_factory=self.logger_factory, runner=self.runner) + compiler.compile() def backup(self): self._backuper.backup() def restore(self): self._backuper.restore() + self._active_source_gresource = self._destination_gresource def move(self): - GresourceMover(self._temp_gresource, self._destination_gresource, logger_factory=self.logger_factory).move() + mover = GresourceMover(self._temp_gresource, self._destination_gresource, + logger_factory=self.logger_factory) + mover.move() diff --git a/scripts/utils/gresource/gresource_backuper.py b/scripts/utils/gresource/gresource_backuper.py index 45beb0b..4c69de0 100644 --- a/scripts/utils/gresource/gresource_backuper.py +++ b/scripts/utils/gresource/gresource_backuper.py @@ -1,6 +1,5 @@ import os import shutil -import subprocess from scripts.utils.gresource import GresourceBackupNotFoundError from scripts.utils.logger.logger import LoggerFactory @@ -23,7 +22,7 @@ class GresourceBackuperManager: class GresourceBackuper: - def __init__(self, destination_file: str, backup_file, logger_factory: LoggerFactory): + def __init__(self, destination_file: str, backup_file: str, logger_factory: LoggerFactory): self.destination_file = destination_file self.backup_file = backup_file self.logger_factory = logger_factory @@ -41,7 +40,6 @@ class GresourceBackuper: os.remove(self.backup_file) shutil.copy2(self.destination_file, self.backup_file) - # subprocess.run(["cp", "-aT", self.destination_file, self.backup_file], check=True) backup_line.success("Backed up gresource files.") @@ -49,4 +47,6 @@ class GresourceBackuper: if not os.path.exists(self.backup_file): raise GresourceBackupNotFoundError(self.backup_file) - subprocess.run(["mv", "-f", self.backup_file, self.destination_file], check=True) + shutil.move(self.backup_file, self.destination_file) + + self.logger_factory.create_logger().success("Restored gresource files.") diff --git a/scripts/utils/gresource/gresource_complier.py b/scripts/utils/gresource/gresource_compiler.py similarity index 87% rename from scripts/utils/gresource/gresource_complier.py rename to scripts/utils/gresource/gresource_compiler.py index 2d7698b..350afde 100644 --- a/scripts/utils/gresource/gresource_complier.py +++ b/scripts/utils/gresource/gresource_compiler.py @@ -1,17 +1,22 @@ -import subprocess import textwrap from pathlib import Path +from scripts.utils.command_runner.command_runner import CommandRunner from scripts.utils.gresource import raise_gresource_error from scripts.utils.logger.logger import LoggerFactory class GresourceCompiler: - def __init__(self, source_folder: str, target_file: str, logger_factory: LoggerFactory): + def __init__( + self, source_folder: str, target_file: str, + logger_factory: LoggerFactory, runner: CommandRunner + ): self.source_folder = source_folder self.target_file = target_file self.gresource_xml = target_file + ".xml" + self.logger_factory = logger_factory + self.runner = runner def compile(self): compile_line = self.logger_factory.create_logger() @@ -55,7 +60,7 @@ class GresourceCompiler: raise def _try_compile_resources(self): - subprocess.run(["glib-compile-resources", + self.runner.run(["glib-compile-resources", "--sourcedir", self.source_folder, "--target", self.target_file, self.gresource_xml diff --git a/scripts/utils/gresource/gresource_extractor.py b/scripts/utils/gresource/gresource_extractor.py index a84e3f8..136c109 100644 --- a/scripts/utils/gresource/gresource_extractor.py +++ b/scripts/utils/gresource/gresource_extractor.py @@ -1,45 +1,48 @@ import os -import subprocess +from scripts.utils.command_runner.command_runner import CommandRunner from scripts.utils.gresource import raise_gresource_error from scripts.utils.logger.logger import LoggerFactory class GresourceExtractor: - def __init__(self, gresource_path: str, extract_folder: str, logger_factory: LoggerFactory): + def __init__( + self, gresource_path: str, extract_folder: str, + logger_factory: LoggerFactory, runner: CommandRunner + ): self.gresource_path = gresource_path self.extract_folder = extract_folder self.logger_factory = logger_factory + self.runner = runner def extract(self): extract_line = self.logger_factory.create_logger() extract_line.update("Extracting gresource files...") - resources = self._get_resources_list() - self._extract_resources(resources) + self._try_extract_resources() extract_line.success("Extracted gresource files.") - def _get_resources_list(self): - resources_list_response = subprocess.run( - ["gresource", "list", self.gresource_path], - capture_output=True, text=True, check=False - ) - - if resources_list_response.stderr: - raise Exception(f"gresource could not process the theme file: {self.gresource_path}") - - return resources_list_response.stdout.strip().split("\n") - - def _extract_resources(self, resources: list[str]): + def _try_extract_resources(self): try: - self._try_extract_resources(resources) + resources = self._get_resources_list() + self._extract_resources(resources) except FileNotFoundError as e: + print(e) if "gresource" in str(e): raise_gresource_error("gresource", e) raise + except Exception as e: + raise Exception(f"gresource could not process the theme file: {self.gresource_path}") from e - def _try_extract_resources(self, resources: list[str]): + def _get_resources_list(self): + resources_list_response = self.runner.run( + ["gresource", "list", self.gresource_path], + capture_output=True, text=True, check=True + ) + return resources_list_response.stdout.strip().split("\n") + + def _extract_resources(self, resources: list[str]): prefix = "/org/gnome/shell/theme/" for resource in resources: resource_path = resource.replace(prefix, "") @@ -47,7 +50,7 @@ class GresourceExtractor: os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'wb') as f: - subprocess.run( + self.runner.run( ["gresource", "extract", self.gresource_path, resource], stdout=f, check=True ) diff --git a/scripts/utils/gresource/gresource_mover.py b/scripts/utils/gresource/gresource_mover.py index 4c8a34a..314c057 100644 --- a/scripts/utils/gresource/gresource_mover.py +++ b/scripts/utils/gresource/gresource_mover.py @@ -1,4 +1,5 @@ -import subprocess +import os +import shutil from scripts.utils.logger.logger import LoggerFactory @@ -13,11 +14,8 @@ class GresourceMover: move_line = self.logger_factory.create_logger() move_line.update("Moving gresource files...") - subprocess.run(["cp", "-f", - self.source_file, - self.destination_file], - check=True) - - subprocess.run(["chmod", "644", self.destination_file], check=True) + os.makedirs(os.path.dirname(self.destination_file), exist_ok=True) + shutil.copyfile(self.source_file, self.destination_file) + os.chmod(self.destination_file, 0o644) move_line.success("Moved gresource files.") diff --git a/tests/_helpers/__init__.py b/tests/_helpers/__init__.py new file mode 100644 index 0000000..ea9510b --- /dev/null +++ b/tests/_helpers/__init__.py @@ -0,0 +1,14 @@ +import os + + +def create_dummy_file(file_path: str, content: str = "dummy content"): + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, 'w') as f: + f.write(content) + + +def try_remove_file(file_path: str): + try: + os.remove(file_path) + except FileNotFoundError: + pass \ No newline at end of file diff --git a/tests/_helpers/dummy_logger_factory.py b/tests/_helpers/dummy_logger_factory.py new file mode 100644 index 0000000..1b2f373 --- /dev/null +++ b/tests/_helpers/dummy_logger_factory.py @@ -0,0 +1,25 @@ +from typing import Optional + +from scripts.utils.logger.logger import LoggerFactory, Logger + + +class DummyLoggerFactory(LoggerFactory): + def create_logger(self, name: Optional[str] = None) -> 'DummyLogger': + return DummyLogger() + + +class DummyLogger(Logger): + def update(self, msg): + pass + + def success(self, msg): + pass + + def error(self, msg): + pass + + def warn(self, msg): + pass + + def info(self, msg): + pass \ No newline at end of file diff --git a/tests/_helpers/dummy_runner.py b/tests/_helpers/dummy_runner.py new file mode 100644 index 0000000..fcff239 --- /dev/null +++ b/tests/_helpers/dummy_runner.py @@ -0,0 +1,6 @@ +from scripts.utils.command_runner.command_runner import CommandRunner + + +class DummyRunner(CommandRunner): + def run(self, command: list[str], **kwargs) -> str: + return "Dummy output" \ No newline at end of file diff --git a/tests/utils/gresource.py b/tests/utils/gresource.py deleted file mode 100644 index 1dbf2bb..0000000 --- a/tests/utils/gresource.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import shutil -import unittest - -import pytest -from unittest.mock import patch, MagicMock -from scripts import config - -from scripts.utils.gresource import Gresource, GresourceBackupNotFoundError, MissingDependencyError - - -class DummyConsoleLine: - def update(self, msg): - pass - def success(self, msg): - pass - - -@patch("scripts.utils.console.Console.Line", return_value=DummyConsoleLine()) -class GresourceTestCase(unittest.TestCase): - def setUp(self): - self.gresource_file = "test.gresource" - self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_temp") - self.destination = os.path.join(config.temp_tests_folder, "gresource_dest") - self.gresource_instance = Gresource(self.gresource_file, self.temp_folder, self.destination) - - def tearDown(self): - self.__try_rmtree(self.temp_folder) - self.__try_rmtree(self.destination) - - def getActiveSourceGresource(self): - return self.gresource_instance._active_source_gresource - - def getBackupGresource(self): - return self.gresource_instance._backup_gresource - - @staticmethod - def __try_rmtree(path): - try: - shutil.rmtree(path) - except FileNotFoundError: - pass - - def test_use_backup_gresource(self, mock_console): - backup_gresource = self.__create_dummy_file(self.getBackupGresource()) - - self.gresource_instance.use_backup_gresource() - - assert self.getActiveSourceGresource() == backup_gresource - - self.__try_remove_file(backup_gresource) - - @staticmethod - def __create_dummy_file(file_path: str): - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, "w") as f: - f.write("dummy content") - return file_path - - @staticmethod - def __try_remove_file(file_path: str): - try: - os.remove(file_path) - except FileNotFoundError: - pass - - def test_use_backup_gresource_not_found(self, mock_console): - self.__try_remove_file(self.getBackupGresource()) - - with pytest.raises(GresourceBackupNotFoundError): - self.gresource_instance.use_backup_gresource() - - def test_extract_calls_correct_methods(self, mock_console): - with ( - patch.object(self.gresource_instance, '_get_resources_list') as mock_get_list, - patch.object(self.gresource_instance, '_extract_resources') as mock_extract - ): - resources = ["resource1", "resource2"] - mock_get_list.return_value = resources - - self.gresource_instance.extract() - - mock_get_list.assert_called_once() - mock_extract.assert_called_once_with(resources) - - def test_get_resources_list(self, mock_console): - """Test that resources are correctly listed from the gresource file.""" - test_resources = ["/org/gnome/shell/theme/file1.css", "/org/gnome/shell/theme/file2.css"] - - with patch("subprocess.run") as mock_run: - mock_run.return_value = self.__mock_gresources_list( - stdout="\n".join(test_resources), - stderr="" - ) - - result = self.gresource_instance._get_resources_list() - - assert result == test_resources - mock_run.assert_called_once() - assert mock_run.call_args[0][0][1] == "list" - - @staticmethod - def __mock_gresources_list(stdout: str, stderr: str): - mock_result = MagicMock() - mock_result.stdout = stdout - mock_result.stderr = stderr - return mock_result - - def test_get_resources_list_error(self, mock_console): - """Test that an exception is raised when gresource fails to list resources.""" - with patch("subprocess.run") as mock_run: - mock_run.return_value = self.__mock_gresources_list( - stdout="", - stderr="Error: gresource failed" - ) - - with pytest.raises(Exception): - self.gresource_instance._get_resources_list() - - def test_extract_resources(self, mock_console): - """Test that resources are correctly extracted.""" - test_resources = [ - "/org/gnome/shell/theme/file1.css", - "/org/gnome/shell/theme/subdir/file2.css" - ] - - with ( - patch("subprocess.run") as mock_run, - patch("os.makedirs") as mock_makedirs, - patch("builtins.open", create=True) - ): - self.gresource_instance._extract_resources(test_resources) - - assert mock_makedirs.call_count == 2 - assert mock_run.call_count == 2 - for i, resource in enumerate(test_resources): - args_list = mock_run.call_args_list[i][0][0] - assert args_list[1] == "extract" - assert args_list[3] == resource - - def test_extract_resources_file_not_found(self, mock_console): - """Test that an Exception is raised when gresource is not found.""" - test_resources = ["/org/gnome/shell/theme/file1.css"] - - with ( - patch("subprocess.run", side_effect=FileNotFoundError("gresource not found")), - patch("builtins.print") - ): - with pytest.raises(MissingDependencyError): - self.gresource_instance._extract_resources(test_resources) - - def test_compile_calls_correct_methods(self, mock_console): - """Test that compile calls the right methods in sequence.""" - with patch.object(self.gresource_instance, '_create_gresource_xml') as mock_create_xml, \ - patch.object(self.gresource_instance, '_compile_resources') as mock_compile: - # Call the method - self.gresource_instance.compile() - - # Verify methods were called correctly in order - mock_create_xml.assert_called_once() - mock_compile.assert_called_once() - - def test_create_gresource_xml(self, mock_console): - """Test that _create_gresource_xml creates the XML file with correct content.""" - with patch("builtins.open", create=True) as mock_open, \ - patch.object(self.gresource_instance, '_generate_gresource_xml') as mock_generate: - mock_generate.return_value = "test content" - mock_file = MagicMock() - mock_open.return_value.__enter__.return_value = mock_file - - # Call the method - self.gresource_instance._create_gresource_xml() - - # Verify file was written with correct content - mock_open.assert_called_once_with(self.gresource_instance._gresource_xml, 'w') - mock_file.write.assert_called_once_with("test content") - - def test_generate_gresource_xml(self, mock_console): - """Test that _generate_gresource_xml creates correct XML structure.""" - with patch.object(self.gresource_instance, '_get_files_to_include') as mock_get_files: - mock_get_files.return_value = ["file1.css", "subdir/file2.css"] - - result = self.gresource_instance._generate_gresource_xml() - - # Check XML structure - assert "" in result - assert "" in result - assert "file1.css" in result - assert "subdir/file2.css" in result - - def test_get_files_to_include(self, mock_console): - """Test that _get_files_to_include finds and formats files correctly.""" - self.__create_dummy_files_in_temp() - - result = self.gresource_instance._get_files_to_include() - - assert len(result) == 2 - assert "file1.css" in result - assert "subdir/file2.css" in result - - def __create_dummy_files_in_temp(self): - os.makedirs(self.temp_folder, exist_ok=True) - test_file1 = os.path.join(self.temp_folder, "file1.css") - test_subdir = os.path.join(self.temp_folder, "subdir") - os.makedirs(test_subdir, exist_ok=True) - test_file2 = os.path.join(test_subdir, "file2.css") - - with open(test_file1, 'w') as f: - f.write("test content") - with open(test_file2, 'w') as f: - f.write("test content") - - def test_compile_resources(self, mock_console): - """Test that _compile_resources runs the correct subprocess command.""" - with patch("subprocess.run") as mock_run: - self.gresource_instance._compile_resources() - - mock_run.assert_called_once() - args = mock_run.call_args[0][0] - assert args[0] == "glib-compile-resources" - assert args[2] == self.temp_folder - assert args[4] == self.gresource_instance._temp_gresource - assert args[5] == self.gresource_instance._gresource_xml - - def test_compile_resources_file_not_found(self, mock_console): - """Test that _compile_resources raises appropriate error when command not found.""" - with patch("subprocess.run", side_effect=FileNotFoundError("glib-compile-resources not found")), \ - patch("builtins.print"): - with pytest.raises(MissingDependencyError): - self.gresource_instance._compile_resources() - - def test_backup_gresource(self, mock_console): - """Test that backup_gresource creates a backup of the gresource file.""" - self.__create_dummy_file(self.gresource_instance._destination_gresource) - - self.gresource_instance.backup() - - assert os.path.exists(self.gresource_instance._backup_gresource) - - def test_restore_gresource(self, mock_console): - """Test that restore_gresource restores the gresource file from backup.""" - self.__create_dummy_file(self.gresource_instance._backup_gresource) - - self.gresource_instance.restore() - - assert os.path.exists(self.gresource_instance._destination_gresource) - assert not os.path.exists(self.gresource_instance._backup_gresource) - - def test_restore_gresource_backup_not_found(self, mock_console): - """Test that restore_gresource raises an error if backup not found.""" - self.__try_remove_file(self.gresource_instance._backup_gresource) - - with pytest.raises(GresourceBackupNotFoundError): - self.gresource_instance.restore() - - def test_move_with_correct_permissions(self, mock_console): - """Test that move changes permissions correctly.""" - self.__create_dummy_file(self.gresource_instance._temp_gresource) - self.__create_dummy_file(self.gresource_instance._destination_gresource) - - with patch("subprocess.run") as mock_run: - self.gresource_instance.move() - - assert os.path.exists(self.gresource_instance._destination_gresource) - permissions = oct(os.stat(self.gresource_instance._destination_gresource).st_mode)[-3:] - assert permissions == "644" - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/tests/utils/gresource/__init__.py b/tests/utils/gresource/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/gresource/test_gresource.py b/tests/utils/gresource/test_gresource.py new file mode 100644 index 0000000..08ba95b --- /dev/null +++ b/tests/utils/gresource/test_gresource.py @@ -0,0 +1,130 @@ +import os.path +import shutil +import unittest +from unittest.mock import patch + +from scripts import config +from scripts.utils.gresource.gresource import Gresource +from ..._helpers import create_dummy_file, try_remove_file +from ..._helpers.dummy_logger_factory import DummyLoggerFactory +from ..._helpers.dummy_runner import DummyRunner + + +class GresourceTestCase(unittest.TestCase): + def setUp(self): + self.gresource_file = "test.gresource" + self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_temp") + self.destination = os.path.join(config.temp_tests_folder, "gresource_dest") + + self.temp_file = os.path.join(self.temp_folder, self.gresource_file) + self.destination_file = os.path.join(self.destination, self.gresource_file) + + self.logger = DummyLoggerFactory() + self.runner = DummyRunner() + + self.gresource = Gresource( + self.gresource_file, self.temp_folder, self.destination, + logger_factory=self.logger, runner=self.runner + ) + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + shutil.rmtree(self.destination, ignore_errors=True) + + def test_use_backup_gresource(self): + destination_file = os.path.join(self.destination, self.gresource_file) + create_dummy_file(destination_file) + self.gresource.backup() + + self.gresource.use_backup_gresource() + + assert self.gresource._active_source_gresource != self.gresource._destination_gresource + assert os.path.exists(self.gresource._active_source_gresource) + + try_remove_file(self.gresource._active_source_gresource) + try_remove_file(destination_file) + + def test_use_backup_gresource_not_found(self): + destination_file = os.path.join(self.destination, self.gresource_file) + try_remove_file(destination_file) + + with self.assertRaises(FileNotFoundError): + self.gresource.use_backup_gresource() + + def test_extract(self): + """Test that extract creates and calls GresourceExtractor correctly.""" + with patch('scripts.utils.gresource.gresource.GresourceExtractor') as mock_extractor_class: + mock_extractor_instance = mock_extractor_class.return_value + + self.gresource.extract() + + mock_extractor_class.assert_called_once_with( + self.gresource._active_source_gresource, + self.temp_folder, + logger_factory=self.logger, + runner=self.runner + ) + mock_extractor_instance.extract.assert_called_once() + + def test_compile(self): + """Test that compile creates and calls GresourceCompiler correctly.""" + with (patch('scripts.utils.gresource.gresource.GresourceCompiler') as mock_compiler_class): + mock_compiler_instance = mock_compiler_class.return_value + + self.gresource.compile() + + mock_compiler_class.assert_called_once_with( + self.temp_folder, + self.gresource._temp_gresource, + logger_factory=self.logger, + runner=self.runner + ) + mock_compiler_instance.compile.assert_called_once() + + def test_backup(self): + create_dummy_file(self.destination_file) + + self.gresource.backup() + backup = self.gresource.use_backup_gresource() + + assert os.path.exists(backup) + self.gresource.restore() + + def test_backup_not_found(self): + try_remove_file(self.destination_file) + + with self.assertRaises(FileNotFoundError): + self.gresource.backup() + + def test_restore(self): + destination_file = os.path.join(self.destination, self.gresource_file) + create_dummy_file(destination_file, content="dummy content") + self.gresource.backup() + create_dummy_file(destination_file, content="new content") + + self.gresource.restore() + + assert os.path.exists(destination_file) + with open(destination_file) as f: + content = f.read() + assert content == "dummy content" + + def test_restore_not_found(self): + destination_file = os.path.join(self.destination, self.gresource_file) + try_remove_file(destination_file) + + with self.assertRaises(FileNotFoundError): + self.gresource.restore() + + def test_move(self): + create_dummy_file(self.temp_file) + + self.gresource.move() + + assert os.path.exists(self.destination_file) + + def test_move_not_found(self): + try_remove_file(self.temp_file) + + with self.assertRaises(FileNotFoundError): + self.gresource.move() \ No newline at end of file diff --git a/tests/utils/gresource/test_gresource_backuper.py b/tests/utils/gresource/test_gresource_backuper.py new file mode 100644 index 0000000..69b0145 --- /dev/null +++ b/tests/utils/gresource/test_gresource_backuper.py @@ -0,0 +1,100 @@ +import os +import shutil +import unittest + +import pytest + +from scripts import config +from scripts.utils.gresource import GresourceBackupNotFoundError +from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager, GresourceBackuper +from ..._helpers import create_dummy_file, try_remove_file +from ..._helpers.dummy_logger_factory import DummyLoggerFactory + + +class GresourceBackuperManagerTestCase(unittest.TestCase): + def setUp(self): + self.gresource_file = "test.gresource" + self.temp_folder = os.path.join(config.temp_tests_folder, "backup_temp") + self.destination = os.path.join(config.temp_tests_folder, "backup_dest") + self.destination_file = os.path.join(self.temp_folder, self.gresource_file) + + self.logger = DummyLoggerFactory() + + self.backuper_manager = GresourceBackuperManager(self.destination_file, + logger_factory=self.logger) + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + shutil.rmtree(self.destination, ignore_errors=True) + + def test_get_backup(self): + create_dummy_file(self.destination_file) + + self.backuper_manager.backup() + backup = self.backuper_manager.get_backup() + + assert os.path.exists(backup) + + def test_backup_overwrites_existing_backup(self): + """Test that backup properly overwrites an existing backup file.""" + create_dummy_file(self.destination_file, content="original") + create_dummy_file(self.backuper_manager._backup_file, content="old backup") + + self.backuper_manager.backup() + + with open(self.backuper_manager._backup_file, 'r') as f: + content = f.read() + assert content == "original" + + +class GresourceBackuperTestCase(unittest.TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "backup_temp") + self.destination_file = os.path.join(self.temp_folder, "test.gresource") + self.backup_file = f"{self.destination_file}.backup" + + self.logger = DummyLoggerFactory() + + self.backuper = GresourceBackuper(self.destination_file, self.backup_file, + logger_factory=self.logger) + + os.makedirs(self.temp_folder, exist_ok=True) + + def test_get_backup(self): + create_dummy_file(self.backup_file) + + backup = self.backuper.get_backup() + + assert os.path.exists(backup) + assert backup == self.backup_file + + def test_use_backup_gresource_not_found(self): + try_remove_file(self.backup_file) + + with pytest.raises(GresourceBackupNotFoundError): + self.backuper.get_backup() + + def test_backup_creates_backup_file(self): + """Test direct backup functionality.""" + create_dummy_file(self.destination_file) + + self.backuper.backup() + + assert os.path.exists(self.backup_file) + + def test_backup_handles_missing_destination(self): + """Test backup behavior when destination file doesn't exist.""" + try_remove_file(self.destination_file) + + with pytest.raises(FileNotFoundError): + self.backuper.backup() + + def test_restore_implementation(self): + """Test direct restore implementation.""" + create_dummy_file(self.backup_file) + try_remove_file(self.destination_file) + + self.backuper.restore() + + assert os.path.exists(self.destination_file) + assert not os.path.exists(self.backup_file) \ No newline at end of file diff --git a/tests/utils/gresource/test_gresource_compiler.py b/tests/utils/gresource/test_gresource_compiler.py new file mode 100644 index 0000000..2973867 --- /dev/null +++ b/tests/utils/gresource/test_gresource_compiler.py @@ -0,0 +1,125 @@ +import os +import shutil +import subprocess +import unittest +from unittest.mock import patch, MagicMock + +import pytest + +from scripts import config +from scripts.utils.gresource import MissingDependencyError +from scripts.utils.gresource.gresource_compiler import GresourceCompiler +from ..._helpers.dummy_logger_factory import DummyLoggerFactory +from ..._helpers.dummy_runner import DummyRunner + + +class GresourceCompilerTestCase(unittest.TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_compiler_temp") + self.target_file = os.path.join(self.temp_folder, "test.gresource") + + self.logger = DummyLoggerFactory() + self.runner = DummyRunner() + + self.compiler = GresourceCompiler(self.temp_folder, self.target_file, + logger_factory=self.logger, runner=self.runner) + + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + + def test_compile_calls_correct_methods(self): + """Test that compile calls the right methods in sequence.""" + with ( + patch.object(self.compiler, '_create_gresource_xml') as mock_create_xml, + patch.object(self.compiler, '_compile_resources') as mock_compile + ): + self.compiler.compile() + + # Verify methods were called correctly in order + mock_create_xml.assert_called_once() + mock_compile.assert_called_once() + + def test_create_gresource_xml(self): + """Test that _create_gresource_xml creates the XML file with correct content.""" + with ( + patch("builtins.open", create=True) as mock_open, + patch.object(self.compiler, '_generate_gresource_xml') as mock_generate + ): + mock_generate.return_value = "test content" + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + + self.compiler._create_gresource_xml() + + mock_open.assert_called_once_with(self.compiler.gresource_xml, 'w') + mock_file.write.assert_called_once_with("test content") + + def test_generate_gresource_xml(self): + """Test that _generate_gresource_xml creates correct XML structure.""" + with patch.object(self.compiler, '_get_files_to_include') as mock_get_files: + mock_get_files.return_value = ["file1.css", "subdir/file2.css"] + + result = self.compiler._generate_gresource_xml() + + assert "" in result + assert "" in result + assert "file1.css" in result + assert "subdir/file2.css" in result + + def test_get_files_to_include(self): + """Test that _get_files_to_include finds and formats files correctly.""" + self.__create_dummy_files_in_temp() + + result = self.compiler._get_files_to_include() + + assert len(result) == 2 + assert "file1.css" in result + assert "subdir/file2.css" in result + + def __create_dummy_files_in_temp(self): + os.makedirs(self.temp_folder, exist_ok=True) + test_file1 = os.path.join(self.temp_folder, "file1.css") + test_subdir = os.path.join(self.temp_folder, "subdir") + os.makedirs(test_subdir, exist_ok=True) + test_file2 = os.path.join(test_subdir, "file2.css") + + with open(test_file1, 'w') as f: + f.write("test content") + with open(test_file2, 'w') as f: + f.write("test content") + + def test_compile_resources(self): + """Test that _compile_resources runs the correct subprocess command.""" + with patch.object(self.runner, "run") as mock_run: + self.compiler._compile_resources() + + mock_run.assert_called_once() + args = mock_run.call_args[0][0] + assert args[0] == "glib-compile-resources" + assert args[2] == self.temp_folder + assert args[4] == self.compiler.target_file + assert args[5] == self.compiler.gresource_xml + + def test_compile_resources_file_not_found(self): + """Test that _compile_resources raises appropriate error when command not found.""" + with ( + patch.object(self.runner, "run", side_effect=FileNotFoundError("glib-compile-resources not found")), + patch("builtins.print") + ): + with pytest.raises(MissingDependencyError): + self.compiler._compile_resources() + + def test_try_compile_resources_called_process_error(self): + """Test handling of subprocess execution failures.""" + process_error = subprocess.CalledProcessError(1, "glib-compile-resources", output="Failed to compile") + with patch.object(self.runner, "run", side_effect=process_error): + with pytest.raises(subprocess.CalledProcessError): + self.compiler._try_compile_resources() + + def test_compile_resources_other_file_not_found_error(self): + """Test that other FileNotFoundError exceptions are propagated.""" + with patch.object(self.runner, "run", side_effect=FileNotFoundError("Some other file not found")): + with pytest.raises(FileNotFoundError): + self.compiler._compile_resources() \ No newline at end of file diff --git a/tests/utils/gresource/test_gresource_extractor.py b/tests/utils/gresource/test_gresource_extractor.py new file mode 100644 index 0000000..88cd613 --- /dev/null +++ b/tests/utils/gresource/test_gresource_extractor.py @@ -0,0 +1,123 @@ +import os +import shutil +import unittest +from unittest.mock import patch, MagicMock + +import pytest + +from scripts import config +from scripts.utils.gresource import MissingDependencyError +from scripts.utils.gresource.gresource_extractor import GresourceExtractor +from ..._helpers.dummy_logger_factory import DummyLoggerFactory +from ..._helpers.dummy_runner import DummyRunner + + +class GresourceExtractorTestCase(unittest.TestCase): + def setUp(self): + self.gresource_file = "test.gresource" + self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_extractor_temp") + self.destination = os.path.join(config.temp_tests_folder, "gresource_extractor_dest") + + self.logger = DummyLoggerFactory() + self.runner = DummyRunner() + + self.extractor = GresourceExtractor( + self.gresource_file, self.temp_folder, + logger_factory=self.logger, runner=self.runner + ) + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + shutil.rmtree(self.destination, ignore_errors=True) + + def test_extract_calls_correct_methods(self): + with ( + patch.object(self.extractor, '_get_resources_list') as mock_get_list, + patch.object(self.extractor, '_extract_resources') as mock_extract + ): + resources = ["resource1", "resource2"] + mock_get_list.return_value = resources + + self.extractor.extract() + + mock_get_list.assert_called_once() + mock_extract.assert_called_once_with(resources) + + def test_get_resources_list(self): + """Test that resources are correctly listed from the gresource file.""" + test_resources = ["/org/gnome/shell/theme/file1.css", "/org/gnome/shell/theme/file2.css"] + + with patch.object(self.runner, "run") as mock_run: + mock_run.return_value = self.__mock_gresources_list( + stdout="\n".join(test_resources), + stderr="" + ) + + result = self.extractor._get_resources_list() + + assert result == test_resources + mock_run.assert_called_once() + assert mock_run.call_args[0][0][1] == "list" + + @staticmethod + def __mock_gresources_list(stdout: str, stderr: str): + mock_result = MagicMock() + mock_result.stdout = stdout + mock_result.stderr = stderr + return mock_result + + def test_get_resources_list_error(self): + """Test that an exception is raised when gresource fails to list resources.""" + with patch.object(self.runner, "run", + side_effect=Exception("Error: gresource failed")): + + with pytest.raises(Exception): + self.extractor._get_resources_list() + + def test_extract_resources(self): + """Test that resources are correctly extracted.""" + test_resources = [ + "/org/gnome/shell/theme/file1.css", + "/org/gnome/shell/theme/subdir/file2.css" + ] + + with ( + patch.object(self.runner, "run") as mock_run, + patch("os.makedirs") as mock_makedirs, + patch("builtins.open", create=True) + ): + self.extractor._extract_resources(test_resources) + + assert mock_makedirs.call_count == 2 + assert mock_run.call_count == 2 + for i, resource in enumerate(test_resources): + args_list = mock_run.call_args_list[i][0][0] + assert args_list[1] == "extract" + assert args_list[3] == resource + + def test_extract_resources_file_not_found(self): + with ( + patch.object(self.runner, "run", + side_effect=FileNotFoundError("gresource not found")), + patch("builtins.print") + ): + with pytest.raises(MissingDependencyError): + self.extractor.extract() + + def test_try_extract_resources(self): + resources = ["/org/gnome/shell/theme/file.css"] + + with ( + patch("os.makedirs"), + patch("builtins.open", create=True) as mock_open + ): + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + + self.extractor._extract_resources(resources) + + expected_path = os.path.join(self.temp_folder, "file.css") + mock_open.assert_called_once_with(expected_path, 'wb') + + def test_empty_resource_list(self): + self.extractor._extract_resources([]) \ No newline at end of file diff --git a/tests/utils/gresource/test_gresource_mover.py b/tests/utils/gresource/test_gresource_mover.py new file mode 100644 index 0000000..9e95f8e --- /dev/null +++ b/tests/utils/gresource/test_gresource_mover.py @@ -0,0 +1,51 @@ +import os.path +import unittest +from unittest.mock import patch + +from scripts import config +from scripts.utils.gresource.gresource_mover import GresourceMover +from ..._helpers import create_dummy_file, try_remove_file +from ..._helpers.dummy_logger_factory import DummyLoggerFactory +from ..._helpers.dummy_runner import DummyRunner + + +class GresourceMoverTestCase(unittest.TestCase): + def setUp(self): + self.gresource_file = "test.gresource" + self.source_file = os.path.join(config.temp_tests_folder, "gresource_mover_source", self.gresource_file) + self.destination_file = os.path.join(config.temp_tests_folder, "gresource_mover_destination", self.gresource_file) + + self.logger = DummyLoggerFactory() + self.runner = DummyRunner() + + self.mover = GresourceMover(self.source_file, self.destination_file, + logger_factory=self.logger) + + + def tearDown(self): + try_remove_file(self.source_file) + try_remove_file(self.destination_file) + + def test_move_with_correct_permissions(self): + """Test that move changes permissions correctly.""" + create_dummy_file(self.source_file) + + self.mover.move() + + assert os.path.exists(self.mover.destination_file) + permissions = oct(os.stat(self.mover.destination_file).st_mode)[-3:] + assert permissions == "644" + + def test_move_handles_cp_error(self): + """Test that errors during copy are properly handled.""" + with patch('shutil.copyfile', side_effect=OSError): + with self.assertRaises(OSError): + self.mover.move() + + def test_move_handles_chmod_error(self): + """Test that errors during chmod are properly handled.""" + create_dummy_file(self.source_file) + + with patch('os.chmod', side_effect=PermissionError): + with self.assertRaises(PermissionError): + self.mover.move() \ No newline at end of file