diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 52b784b..4991aba 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 scripts/tests.py + pytest tests/*.py tests/*/*.py diff --git a/scripts/config.py b/scripts/config.py index 8a0e6f1..911e512 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -3,6 +3,7 @@ from tempfile import gettempdir # folder definitions temp_folder = f"{gettempdir()}/marble" +temp_tests_folder = f"{temp_folder}/tests" gdm_folder = "gdm" gnome_folder = "gnome-shell" temp_gnome_folder = f"{temp_folder}/{gnome_folder}" diff --git a/scripts/tests.py b/scripts/tests.py deleted file mode 100644 index 9130560..0000000 --- a/scripts/tests.py +++ /dev/null @@ -1,70 +0,0 @@ -# TODO: Add more tests - -import unittest -import os -import json -import shutil -from unittest.mock import patch - -from . import config -from .theme import Theme - -# folders -tests_folder = '.tests' -project_folder = '.' - - -class TestInstall(unittest.TestCase): - def setUp(self): - # Create necessary directories - os.makedirs(f"{project_folder}/{config.raw_theme_folder}/{config.gnome_folder}", exist_ok=True) - os.makedirs(f"{tests_folder}/.themes", exist_ok=True) - os.makedirs(f"{tests_folder}/.temp", exist_ok=True) - - def tearDown(self): - # Clean up after tests - shutil.rmtree(tests_folder, ignore_errors=True) - - @patch('scripts.utils.gnome.subprocess.check_output') - def test_install_theme(self, mock_check_output): - """ - Test if theme is installed correctly (colors are replaced) - """ - mock_check_output.return_value = 'GNOME Shell 47.0\n' - - # folders - themes_folder = f"{tests_folder}/.themes" - temp_folder = f"{tests_folder}/.temp" - - # colors from colors.json - colors_json = open(f"{project_folder}/{config.colors_json}") - colors = json.load(colors_json) - colors_json.close() - - # create test theme - test_theme = Theme("gnome-shell", colors, - f"{project_folder}/{config.raw_theme_folder}/{config.gnome_folder}", - themes_folder, temp_folder, - mode='light', is_filled=True) - - # install test theme - test_theme.install(120, 'test', 70) - - # folder with installed theme (.tests/.themes/Marble-test-light/gnome-shell) - installed_theme = f"{themes_folder}/{os.listdir(themes_folder)[0]}/{config.gnome_folder}" - - # check if files are installed - for file in os.listdir(installed_theme): - with open(f"{installed_theme}/{file}") as f: - read_file = f.read() - - for color in colors["elements"]: - self.assertNotIn(color, read_file, msg=f"Color {color} is not replaced in {file}") - - # delete test theme - del test_theme - shutil.rmtree(tests_folder) - - -if __name__ == '__main__': - unittest.main() diff --git a/scripts/utils/gresource.py b/scripts/utils/gresource.py index 8d15657..9091c19 100644 --- a/scripts/utils/gresource.py +++ b/scripts/utils/gresource.py @@ -13,6 +13,11 @@ class GresourceBackupNotFoundError(FileNotFoundError): else: super().__init__("Gresource backup file not found.") +class MissingDependencyError(Exception): + def __init__(self, dependency: str): + super().__init__(f"Missing required dependency: {dependency}") + self.dependency = dependency + class Gresource: """Handles the extraction and compilation of gresource files for GNOME Shell themes.""" @@ -27,38 +32,38 @@ class Gresource: self.temp_folder = temp_folder self.destination = destination - 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.__backup_gresource = os.path.join(destination, f"{gresource_file}.backup") - self.__gresource_xml = os.path.join(temp_folder, f"{gresource_file}.xml") + 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._backup_gresource = os.path.join(destination, f"{gresource_file}.backup") + self._gresource_xml = os.path.join(temp_folder, f"{gresource_file}.xml") def use_backup_gresource(self): - if not os.path.exists(self.__backup_gresource): - raise GresourceBackupNotFoundError(self.__backup_gresource) - self.__active_source_gresource = self.__backup_gresource + if not os.path.exists(self._backup_gresource): + raise GresourceBackupNotFoundError(self._backup_gresource) + self._active_source_gresource = self._backup_gresource def extract(self): extract_line = Console.Line() extract_line.update("Extracting gresource files...") - resources = self.__get_resources_list() - self.__extract_resources(resources) + resources = self._get_resources_list() + self._extract_resources(resources) extract_line.success("Extracted gresource files.") - def __get_resources_list(self): + def _get_resources_list(self): resources_list_response = subprocess.run( - ["gresource", "list", self.__active_source_gresource], + ["gresource", "list", self._active_source_gresource], capture_output=True, text=True, check=False ) if resources_list_response.stderr: - raise Exception(f"gresource could not process the theme file: {self.__active_source_gresource}") + raise Exception(f"gresource could not process the theme file: {self._active_source_gresource}") return resources_list_response.stdout.strip().split("\n") - def __extract_resources(self, resources: list[str]): + def _extract_resources(self, resources: list[str]): prefix = "/org/gnome/shell/theme/" try: for resource in resources: @@ -68,38 +73,38 @@ class Gresource: with open(output_path, 'wb') as f: subprocess.run( - ["gresource", "extract", self.__active_source_gresource, resource], + ["gresource", "extract", self._active_source_gresource, resource], stdout=f, check=True ) except FileNotFoundError as e: if "gresource" in str(e): - self.__raise_gresource_error(e) + self._raise_gresource_error(e) raise @staticmethod - def __raise_gresource_error(e: Exception): + def _raise_gresource_error(e: Exception): print("Error: 'gresource' command not found.") print("Please install the glib2-devel package:") print(" - For Fedora/RHEL: sudo dnf install glib2-devel") print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev") print(" - For Arch: sudo pacman -S glib2-devel") - raise Exception("Missing required dependency: glib2-devel") from e + raise MissingDependencyError("glib2-devel") from e def compile(self): compile_line = Console.Line() compile_line.update("Compiling gnome-shell theme...") - self.__create_gresource_xml() - self.__compile_resources() + self._create_gresource_xml() + self._compile_resources() compile_line.success("Theme compiled.") - def __create_gresource_xml(self): - with open(self.__gresource_xml, 'w') as gresource_xml: - gresource_xml.write(self.__generate_gresource_xml()) + def _create_gresource_xml(self): + with open(self._gresource_xml, 'w') as gresource_xml: + gresource_xml.write(self._generate_gresource_xml()) - def __generate_gresource_xml(self): - files_to_include = self.__get_files_to_include() + def _generate_gresource_xml(self): + files_to_include = self._get_files_to_include() nl = "\n" # fstring doesn't support newline character return textwrap.dedent(f""" @@ -110,7 +115,7 @@ class Gresource: """) - def __get_files_to_include(self): + def _get_files_to_include(self): temp_path = Path(self.temp_folder) return [ f"{file.relative_to(temp_path)}" @@ -118,17 +123,17 @@ class Gresource: if file.is_file() ] - def __compile_resources(self): + def _compile_resources(self): try: subprocess.run(["glib-compile-resources", "--sourcedir", self.temp_folder, - "--target", self.__temp_gresource, - self.__gresource_xml + "--target", self._temp_gresource, + self._gresource_xml ], cwd=self.temp_folder, check=True) except FileNotFoundError as e: if "glib-compile-resources" in str(e): - self.__raise_gresource_error(e) + self._raise_gresource_error(e) raise def backup(self): @@ -136,19 +141,19 @@ class Gresource: backup_line.update("Backing up gresource files...") subprocess.run(["cp", "-aT", - self.__destination_gresource, - self.__backup_gresource], + self._destination_gresource, + self._backup_gresource], check=True) backup_line.success("Backed up gresource files.") def restore(self): - if not os.path.exists(self.__backup_gresource): - raise GresourceBackupNotFoundError(self.__backup_gresource) + if not os.path.exists(self._backup_gresource): + raise GresourceBackupNotFoundError(self._backup_gresource) - subprocess.run(["sudo", "mv", "-f", - self.__backup_gresource, - self.__destination_gresource], + subprocess.run(["mv", "-f", + self._backup_gresource, + self._destination_gresource], check=True) @@ -156,9 +161,11 @@ class Gresource: move_line = Console.Line() move_line.update("Moving gresource files...") - subprocess.run(["sudo", "cp", "-f", - self.__temp_gresource, - self.__destination_gresource], + subprocess.run(["cp", "-f", + self._temp_gresource, + self._destination_gresource], check=True) + subprocess.run(["chmod", "644", self._destination_gresource], check=True) + move_line.success("Moved gresource files.") \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/gresource.py b/tests/utils/gresource.py new file mode 100644 index 0000000..1dbf2bb --- /dev/null +++ b/tests/utils/gresource.py @@ -0,0 +1,271 @@ +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