diff --git a/install.py b/install.py index 1c2f84c..5d3b6c0 100644 --- a/install.py +++ b/install.py @@ -1,6 +1,6 @@ # This file installs Marble shell theme for GNOME DE # Copyright (C) 2023-2025 Vladyslav Hroshev - +import os # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -22,6 +22,7 @@ from scripts.install.colors_definer import ColorsDefiner from scripts.install.global_theme_installer import GlobalThemeInstaller from scripts.install.local_theme_installer import LocalThemeInstaller from scripts.utils.gnome import apply_gnome_theme +from scripts.utils.logger.console import Console def main(): @@ -31,6 +32,12 @@ def main(): installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller installer = installer_class(args, colors_definer) + if args.gdm: + if os.getuid() != 0: + Console().Line().error( + "Global installation requires root privileges. Please run the script as root.") + return + if args.remove or args.reinstall: installer.remove() diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py index 0550081..77e3470 100644 --- a/scripts/install/global_theme_installer.py +++ b/scripts/install/global_theme_installer.py @@ -1,6 +1,6 @@ from scripts.install.theme_installer import ThemeInstaller from scripts.utils.global_theme.gdm import GDMTheme -from scripts.utils.global_theme.gdm_builder import GdmBuilder +from scripts.utils.global_theme.gdm_builder import GDMThemeBuilder from scripts.utils.logger.console import Console, Color, Format @@ -13,7 +13,7 @@ class GlobalThemeInstaller(ThemeInstaller): print("GDM theme removed successfully.") def _define_theme(self): - gdm_builder = GdmBuilder(self.colors) + gdm_builder = GDMThemeBuilder(self.colors) gdm_builder.with_mode(self.args.mode) gdm_builder.with_filled(self.args.filled) self.theme = gdm_builder.build() diff --git a/scripts/utils/files_labeler.py b/scripts/utils/files_labeler.py index 6243f7a..1107f45 100644 --- a/scripts/utils/files_labeler.py +++ b/scripts/utils/files_labeler.py @@ -1,15 +1,28 @@ import os +from abc import ABC, abstractmethod from typing import Tuple, TypeAlias LabeledFileGroup: TypeAlias = Tuple[str, str] + +class FilesLabelerFactory(ABC): + @abstractmethod + def create(self, temp_folder: str, *files_to_update_references: str) -> 'FilesLabeler': + pass + + +class FilesLabelerFactoryImpl(FilesLabelerFactory): + def create(self, temp_folder: str, *files_to_update_references: str) -> 'FilesLabeler': + return FilesLabeler(temp_folder, *files_to_update_references) + + class FilesLabeler: - def __init__(self, directory: str, *args: str): + def __init__(self, directory: str, *files_to_update_references: str): """ Initialize the working directory and files to change """ self.directory = directory - self.files = args + self.files = files_to_update_references def append_label(self, label: str): """ diff --git a/scripts/utils/global_theme/gdm_builder.py b/scripts/utils/global_theme/gdm_builder.py index edf87f1..31ea366 100644 --- a/scripts/utils/global_theme/gdm_builder.py +++ b/scripts/utils/global_theme/gdm_builder.py @@ -6,6 +6,7 @@ from scripts.install.colors_definer import ColorsDefiner from scripts.types.installation_color import InstallationMode from scripts.utils.alternatives_updater import AlternativesUpdater, PathString from scripts.utils.command_runner.subprocess_command_runner import SubprocessCommandRunner +from scripts.utils.files_labeler import FilesLabelerFactoryImpl from scripts.utils.global_theme.gdm import GDMTheme from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer @@ -17,7 +18,7 @@ from scripts.utils.logger.logger import LoggerFactory from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder -class GdmBuilder: +class GDMThemeBuilder: """ Builder class for creating GDMTheme instances with configurable components. @@ -26,7 +27,7 @@ class GdmBuilder: automatically resolved during build() if not provided. Example usage: - builder = GdmBuilder(colors_provider) + builder = GMDThemeBuilder(colors_provider) theme = builder.with_mode("dark").with_filled(True).build() """ def __init__(self, colors_provider: ColorsDefiner): @@ -46,42 +47,42 @@ class GdmBuilder: self._installer: Optional[GDMThemeInstaller] = None self._remover: Optional[GDMThemeRemover] = None - def with_mode(self, mode: InstallationMode | None) -> 'GdmBuilder': + def with_mode(self, mode: InstallationMode | None) -> 'GDMThemeBuilder': """Set the mode for the theme.""" self._mode = mode return self - def with_filled(self, is_filled=True) -> 'GdmBuilder': + def with_filled(self, is_filled=True) -> 'GDMThemeBuilder': """Set the filled state for the theme.""" self._is_filled = is_filled return self - def with_logger_factory(self, logger_factory: LoggerFactory) -> 'GdmBuilder': + def with_logger_factory(self, logger_factory: LoggerFactory) -> 'GDMThemeBuilder': """Inject a logger factory for logging purposes.""" self._logger_factory = logger_factory return self - def with_gresource(self, gresource: Gresource) -> 'GdmBuilder': + def with_gresource(self, gresource: Gresource) -> 'GDMThemeBuilder': """Inject a gresource instance for managing gresource files.""" self._gresource = gresource return self - def with_ubuntu_gdm_alternatives_updater(self, alternatives_updater: UbuntuGDMAlternativesUpdater) -> 'GdmBuilder': + def with_ubuntu_gdm_alternatives_updater(self, alternatives_updater: UbuntuGDMAlternativesUpdater) -> 'GDMThemeBuilder': """Inject an alternatives updater for managing GDM alternatives.""" self._ubuntu_gdm_alternatives_updater = alternatives_updater return self - def with_preparer(self, preparer: GDMThemePreparer) -> 'GdmBuilder': + def with_preparer(self, preparer: GDMThemePreparer) -> 'GDMThemeBuilder': """Inject a preparer for preparing the theme.""" self._preparer = preparer return self - def with_installer(self, installer: GDMThemeInstaller) -> 'GdmBuilder': + def with_installer(self, installer: GDMThemeInstaller) -> 'GDMThemeBuilder': """Inject an installer for installing the theme.""" self._installer = installer return self - def with_remover(self, remover: GDMThemeRemover) -> 'GdmBuilder': + def with_remover(self, remover: GDMThemeRemover) -> 'GDMThemeBuilder': """Inject a remover for removing the theme.""" self._remover = remover return self @@ -141,13 +142,15 @@ class GdmBuilder: """Create a GDMThemePreparer if not explicitly provided.""" if self._preparer: return theme_builder = GnomeShellThemeBuilder(self.colors_provider) + files_labeler_factory = FilesLabelerFactoryImpl() self._preparer = GDMThemePreparer( temp_folder=self._temp_folder, default_mode=self._mode, is_filled=self._is_filled, gresource=self._gresource, theme_builder=theme_builder, - logger_factory=self._logger_factory + logger_factory=self._logger_factory, + files_labeler_factory=files_labeler_factory, ) def _resolve_installer(self): diff --git a/scripts/utils/global_theme/gdm_preparer.py b/scripts/utils/global_theme/gdm_preparer.py index a585c8f..3af77e6 100644 --- a/scripts/utils/global_theme/gdm_preparer.py +++ b/scripts/utils/global_theme/gdm_preparer.py @@ -1,5 +1,6 @@ import os +from scripts.utils.files_labeler import FilesLabelerFactory from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare from scripts.utils.gresource.gresource import Gresource from scripts.utils.logger.logger import LoggerFactory @@ -19,13 +20,16 @@ class GDMThemePreparer: def __init__(self, temp_folder: str, default_mode: str | None, is_filled: bool, gresource: Gresource, theme_builder: GnomeShellThemeBuilder, - logger_factory: LoggerFactory): + logger_factory: LoggerFactory, + files_labeler_factory: FilesLabelerFactory): """ :param temp_folder: Temporary folder for extracted theme files :param default_mode: Default theme mode to use if not specified in CSS filename - :param is_filled: Whether to generate filled (True) or outlined (False) styles + :param is_filled: Whether to generate filled (True) or dimmed (False) styles :param gresource: Gresource instance for managing gresource files :param theme_builder: Theme builder instance for creating themes + :param logger_factory: Logger factory for logging messages + :param files_labeler_factory: Factory for creating FilesLabeler instances """ self.temp_folder = temp_folder self.gresource_temp_folder = gresource.temp_folder @@ -36,6 +40,7 @@ class GDMThemePreparer: self.gresource = gresource self.theme_builder = theme_builder self.logger_factory = logger_factory + self.files_labeler_factory = files_labeler_factory def use_backup_as_source(self): """Use backup gresource file for extraction""" @@ -72,11 +77,12 @@ class GDMThemePreparer: theme.prepare() theme_file = os.path.join(self.gresource_temp_folder, file_name) - return GDMThemePrepare(theme=theme, theme_file=theme_file, label=mode) + files_labeler = self.files_labeler_factory.create( + theme.temp_folder, theme.main_styles) + return GDMThemePrepare( + theme=theme, theme_file=theme_file, label=mode, files_labeler=files_labeler) def _setup_theme_builder(self, file_name: str, mode: str): - self.theme_builder.with_temp_folder(self.temp_folder) - theme_name = file_name.replace(".css", "") (self.theme_builder diff --git a/scripts/utils/global_theme/gdm_theme_prepare.py b/scripts/utils/global_theme/gdm_theme_prepare.py index b261e33..66db0d7 100644 --- a/scripts/utils/global_theme/gdm_theme_prepare.py +++ b/scripts/utils/global_theme/gdm_theme_prepare.py @@ -12,15 +12,18 @@ class GDMThemePrepare: - CSS property and keyword removal for customization - Theme installation with color adjustments """ - def __init__(self, theme: Theme, theme_file: str, label: str = None): + def __init__(self, theme: Theme, theme_file: str, label: str | None, + files_labeler: FilesLabeler): """ :param theme: The theme object to prepare :param theme_file: Path to the original decompiled CSS file :param label: Optional label for the theme (e.g. "dark", "light") + :param files_labeler: FilesLabeler instance for labeling files """ self.theme = theme self.theme_file = theme_file self.label = label + self.files_labeler = files_labeler def label_theme(self): """ @@ -31,8 +34,7 @@ class GDMThemePrepare: if self.label is None: raise ValueError("Label is not set for the theme.") - files_labeler = FilesLabeler(self.theme.temp_folder, self.theme.main_styles) - files_labeler.append_label(self.label) + self.files_labeler.append_label(self.label) def remove_keywords(self, *args: str): """Remove specific keywords from the theme file""" @@ -52,7 +54,7 @@ class GDMThemePrepare: :param trigger: String marker used to identify installed themes """ with open(self.theme_file, 'r') as gnome_theme: - gnome_styles = gnome_theme.read() + trigger + gnome_styles = gnome_theme.read() + '\n' + trigger + '\n' self.theme.add_to_start(gnome_styles) def install(self, hue: int, color: str, sat: int | None, destination: str): diff --git a/scripts/utils/global_theme/ubuntu_alternatives_updater.py b/scripts/utils/global_theme/ubuntu_alternatives_updater.py index c067d9a..351cb5d 100644 --- a/scripts/utils/global_theme/ubuntu_alternatives_updater.py +++ b/scripts/utils/global_theme/ubuntu_alternatives_updater.py @@ -17,7 +17,7 @@ class UbuntuGDMAlternativesUpdater: """ :param alternatives_updater: Handler for update-alternatives operations """ - self.ubuntu_gresource_link = config.ubuntu_gresource_link + self.ubuntu_gresource_link_name = config.ubuntu_gresource_link self.destination_dir = config.global_gnome_shell_theme self.destination_file = config.gnome_shell_gresource @@ -26,7 +26,7 @@ class UbuntuGDMAlternativesUpdater: self._update_gresource_paths() def _update_gresource_paths(self): - self.ubuntu_gresource_path = os.path.join(self.destination_dir, self.ubuntu_gresource_link) + self.ubuntu_gresource_path = os.path.join(self.destination_dir, self.ubuntu_gresource_link_name) self.gnome_gresource_path = os.path.join(self.destination_dir, self.destination_file) def with_custom_destination(self, destination_dir: str, destination_file: str): @@ -47,7 +47,7 @@ class UbuntuGDMAlternativesUpdater: """ self.alternatives_updater.install_and_set( link=self.ubuntu_gresource_path, - name=self.ubuntu_gresource_link, + name=self.ubuntu_gresource_link_name, path=self.gnome_gresource_path, priority=priority ) @@ -60,6 +60,6 @@ class UbuntuGDMAlternativesUpdater: the system to fall back to the default GDM theme. """ self.alternatives_updater.remove( - name=self.ubuntu_gresource_link, + name=self.ubuntu_gresource_link_name, path=self.gnome_gresource_path ) diff --git a/scripts/utils/remove_properties.py b/scripts/utils/remove_properties.py index eed79ce..f20ef5e 100644 --- a/scripts/utils/remove_properties.py +++ b/scripts/utils/remove_properties.py @@ -10,10 +10,10 @@ def remove_properties(file, *args): with open(file, "r") as read_file: content = read_file.read() - for line in content.splitlines(): + for i, line in enumerate(content.splitlines()): if not any(prop in line for prop in args): new_content += line + "\n" - elif "}" in line: + elif "}" in line and not "{" in line: new_content += "}\n" with open(file, "w") as write_file: diff --git a/tests/utils/global_theme/__init__.py b/tests/utils/global_theme/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/global_theme/test_gdm.py b/tests/utils/global_theme/test_gdm.py new file mode 100644 index 0000000..4906086 --- /dev/null +++ b/tests/utils/global_theme/test_gdm.py @@ -0,0 +1,81 @@ +from unittest import TestCase +from unittest.mock import Mock + +from scripts.utils.global_theme.gdm import GDMTheme +from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller +from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer +from scripts.utils.global_theme.gdm_remover import GDMThemeRemover + + +class GDMTestCase(TestCase): + def setUp(self): + self.preparer = Mock(spec=GDMThemePreparer) + self.installer = Mock(spec=GDMThemeInstaller) + self.remover = Mock(spec=GDMThemeRemover) + + self.gdm = GDMTheme(self.preparer, self.installer, self.remover) + + def test_prepare_uses_backup_if_installed(self): + self.installer.is_installed.return_value = True + + self.gdm.prepare() + + self.preparer.use_backup_as_source.assert_called_once() + + def test_prepare_does_not_use_backup_if_not_installed(self): + self.installer.is_installed.return_value = False + + self.gdm.prepare() + + self.preparer.use_backup_as_source.assert_not_called() + + def test_prepare_calls_preparer_prepare_and_sets_themes(self): + mock_theme = Mock() + self.preparer.prepare.return_value = [mock_theme] + + self.gdm.prepare() + + self.preparer.prepare.assert_called_once() + self.assertEqual(self.gdm.themes, [mock_theme]) + + def test_install_correctly_passes_arguments_to_installer_compile(self): + hue = 100 + name = "test_theme" + sat = 0.5 + + self.gdm.install(hue, name, sat) + + self.installer.compile.assert_called_once_with(self.gdm.themes, hue, name, sat) + + def test_install_calls_installer_backup_if_not_installed(self): + self.installer.is_installed.return_value = False + + self.gdm.install(100, "test_theme") + + self.installer.backup.assert_called_once() + + def test_install_does_not_call_installer_backup_if_installed(self): + self.installer.is_installed.return_value = True + + self.gdm.install(100, "test_theme") + + self.installer.backup.assert_not_called() + + def test_install_calls_installer_install(self): + self.gdm.install(100, "test_theme") + + self.installer.install.assert_called_once() + + def test_remove_calls_installer_remove_if_installed(self): + self.installer.is_installed.return_value = True + + self.gdm.remove() + + self.remover.remove.assert_called_once() + + def test_remove_calls_installer_warn_if_not_installed(self): + self.installer.is_installed.return_value = False + + self.gdm.remove() + + self.remover.remove.assert_not_called() \ No newline at end of file diff --git a/tests/utils/global_theme/test_gdm_builder.py b/tests/utils/global_theme/test_gdm_builder.py new file mode 100644 index 0000000..e01012a --- /dev/null +++ b/tests/utils/global_theme/test_gdm_builder.py @@ -0,0 +1,155 @@ +from unittest import TestCase +from unittest.mock import Mock + +from scripts.types.installation_color import InstallationMode +from scripts.utils.global_theme.gdm_builder import GDMThemeBuilder + + +class GDMBuilderTestCase(TestCase): + def setUp(self): + self.colors_provider = Mock() + self.builder = GDMThemeBuilder(colors_provider=self.colors_provider) + + def test_with_mode_sets_correct_mode(self): + self.builder._mode = None + mode: InstallationMode = "dark" + + builder = self.builder.with_mode(mode) + + self.assertEqual(builder._mode, mode) + + def test_with_filled_sets_correct_filled_state(self): + self.builder._is_filled = False + is_filled = True + + builder = self.builder.with_filled(is_filled) + + self.assertEqual(builder._is_filled, is_filled) + + def test_with_logger_factory_sets_specified_logger_factory(self): + logger_factory = Mock() + builder = self.builder.with_logger_factory(logger_factory) + self.assertEqual(builder._logger_factory, logger_factory) + + def test_with_gresource_sets_specified_gresource(self): + gresource = Mock() + builder = self.builder.with_gresource(gresource) + self.assertEqual(builder._gresource, gresource) + + def test_with_ubuntu_gdm_alternatives_updater_sets_specified_updater(self): + alternatives_updater = Mock() + builder = self.builder.with_ubuntu_gdm_alternatives_updater(alternatives_updater) + self.assertEqual(builder._ubuntu_gdm_alternatives_updater, alternatives_updater) + + def test_with_preparer_sets_specified_preparer(self): + preparer = Mock() + builder = self.builder.with_preparer(preparer) + self.assertEqual(builder._preparer, preparer) + + def test_with_installer_sets_specified_installer(self): + installer = Mock() + builder = self.builder.with_installer(installer) + self.assertEqual(builder._installer, installer) + + def test_with_remover_sets_specified_remover(self): + remover = Mock() + builder = self.builder.with_remover(remover) + self.assertEqual(builder._remover, remover) + + def test_resolve_logger_factory_initializes_logger_factory(self): + self.builder._logger_factory = None + + self.builder._resolve_logger_factory() + + self.assertIsNotNone(self.builder._logger_factory) + + def test_resolve_gresource_initializes_gresource(self): + self.builder._logger_factory = Mock() + self.builder._gresource = None + + self.builder._resolve_gresource() + + self.assertIsNotNone(self.builder._gresource) + + def test_builder_supports_chaining(self): + theme = self.builder.with_mode("dark").with_filled(True).build() + + self.assertIsNotNone(theme) + + def test_resolve_ubuntu_gdm_alternatives_updater_initializes_gresource(self): + self.builder._logger_factory = Mock() + self.builder._gresource = Mock() + self.builder._ubuntu_gdm_alternatives_updater = None + self.builder._resolve_ubuntu_gdm_alternatives_updater() + self.assertIsNotNone(self.builder._ubuntu_gdm_alternatives_updater) + + def test_resolve_preparer_initializes_preparer(self): + self.builder._logger_factory = Mock() + self.builder._gresource = Mock() + self.builder._ubuntu_gdm_alternatives_updater = Mock() + self.builder._preparer = None + + self.builder._resolve_preparer() + + self.assertIsNotNone(self.builder._preparer) + + def test_resolve_installer_initializes_installer(self): + self.builder._gresource = Mock() + self.builder._ubuntu_gdm_alternatives_updater = Mock() + self.builder._installer = None + + self.builder._resolve_installer() + + self.assertIsNotNone(self.builder._installer) + + def test_resolve_remover_initializes_remover(self): + self.builder._gresource = Mock() + self.builder._ubuntu_gdm_alternatives_updater = Mock() + self.builder._remover = None + + self.builder._resolve_remover() + + self.assertIsNotNone(self.builder._remover) + + def test_build_resolves_dependencies(self): + self.builder._resolve_logger_factory = Mock() + self.builder._resolve_gresource = Mock() + self.builder._resolve_ubuntu_gdm_alternatives_updater = Mock() + self.builder._resolve_preparer = Mock() + self.builder._resolve_installer = Mock() + self.builder._resolve_remover = Mock() + + self.builder.build() + + self.builder._resolve_logger_factory.assert_called_once() + self.builder._resolve_gresource.assert_called_once() + self.builder._resolve_ubuntu_gdm_alternatives_updater.assert_called_once() + self.builder._resolve_preparer.assert_called_once() + self.builder._resolve_installer.assert_called_once() + self.builder._resolve_remover.assert_called_once() + + def test_build_correctly_builds_gdm_theme(self): + self.builder._preparer = Mock() + self.builder._installer = Mock() + self.builder._remover = Mock() + + result = self.builder.build() + + self.assertEqual(result.preparer, self.builder._preparer) + self.assertEqual(result.installer, self.builder._installer) + self.assertEqual(result.remover, self.builder._remover) + + def test_build_with_explicit_dependencies_works_correctly(self): + preparer = Mock() + installer = Mock() + remover = Mock() + builder = (self.builder + .with_preparer(preparer) + .with_installer(installer) + .with_remover(remover)) + + result = builder.build() + + self.assertEqual(result.preparer, preparer) + self.assertEqual(result.installer, installer) + self.assertEqual(result.remover, remover) \ No newline at end of file diff --git a/tests/utils/global_theme/test_gdm_installer.py b/tests/utils/global_theme/test_gdm_installer.py new file mode 100644 index 0000000..79bc558 --- /dev/null +++ b/tests/utils/global_theme/test_gdm_installer.py @@ -0,0 +1,87 @@ +import os.path +from unittest import TestCase +from unittest.mock import MagicMock + +from scripts import config +from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller + + +class GDMInstallerTestCase(TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_installer") + self.gresource = MagicMock() + self.gresource.temp_folder = self.temp_folder + + self.alternatives_updater = MagicMock() + + self.gdm_installer = GDMThemeInstaller( + gresource=self.gresource, + alternatives_updater=self.alternatives_updater + ) + + def test_is_installed_return_the_same_value_as_gresource(self): + self.gresource.has_trigger.return_value = True + + result = self.gdm_installer.is_installed() + + self.assertTrue(result) + self.gresource.has_trigger.assert_called_once() + + def test_compile_does_not_call_label_theme_if_label_is_none(self): + theme_prepare = MagicMock() + theme_prepare.label = None + theme_prepare.label_theme = MagicMock() + + self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None) + + theme_prepare.label_theme.assert_not_called() + + def test_compile_calls_label_theme_if_label_is_set(self): + theme_prepare = MagicMock() + theme_prepare.label = "dark" + theme_prepare.label_theme = MagicMock() + + self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None) + + theme_prepare.label_theme.assert_called_once() + + def test_compile_calls_removes_keywords_and_properties_and_prepends_source_styles(self): + theme_prepare = MagicMock() + theme_prepare.remove_keywords = MagicMock() + theme_prepare.remove_properties = MagicMock() + theme_prepare.prepend_source_styles = MagicMock() + + self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None) + + theme_prepare.remove_keywords.assert_called_once() + theme_prepare.remove_properties.assert_called_once() + theme_prepare.prepend_source_styles.assert_called_once() + + def test_compile_installs_themes_with_correct_parameters(self): + theme_prepare = MagicMock() + theme_prepare.install = MagicMock() + themes = [theme_prepare] + hue = 0 + color = "red" + sat = None + + self.gdm_installer.compile(themes, hue, color, sat) + + theme_prepare.install.assert_called_once() + theme_prepare.install.assert_called_with(hue, color, sat, destination=self.temp_folder) + + def test_compile_calls_gresource_compile(self): + self.gdm_installer.compile([], 0, "red", None) + + self.gresource.compile.assert_called_once() + + def test_backup_calls_gresource_backup(self): + self.gdm_installer.backup() + + self.gresource.backup.assert_called_once() + + def test_install_calls_gresource_move_and_alternatives_updater_install_and_set(self): + self.gdm_installer.install() + + self.gresource.move.assert_called_once() + self.alternatives_updater.install_and_set.assert_called_once() \ No newline at end of file diff --git a/tests/utils/global_theme/test_gdm_preparer.py b/tests/utils/global_theme/test_gdm_preparer.py new file mode 100644 index 0000000..99748ce --- /dev/null +++ b/tests/utils/global_theme/test_gdm_preparer.py @@ -0,0 +1,124 @@ +import os +import shutil +import unittest +from unittest.mock import MagicMock, patch + +from scripts import config +from scripts.types.theme_base import ThemeBase +from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer + + +class DummyTheme(ThemeBase): + def __init__(self): + super().__init__() + self.temp_folder = None + self.main_styles = None + + def prepare(self): + pass + + def install(self, hue: int, name: str, sat: float | None = None): + pass + + +class TestGDMThemePreparer(unittest.TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_preparer") + + self.gresource = self._mock_gresource(self.temp_folder) + self.theme_builder = self._mock_builder() + + self.mock_logger = MagicMock() + self.logger_factory = MagicMock() + self.logger_factory.create_logger.return_value = self.mock_logger + + self.preparer = GDMThemePreparer( + temp_folder=self.temp_folder, + default_mode="light", + is_filled=True, + gresource=self.gresource, + theme_builder=self.theme_builder, + logger_factory=self.logger_factory, + files_labeler_factory=MagicMock(), + ) + + @staticmethod + def _mock_gresource(temp_folder): + gresource = MagicMock() + gresource.temp_folder = temp_folder + gresource.extract = MagicMock() + gresource.use_backup_gresource = MagicMock() + return gresource + + @staticmethod + def _mock_builder(): + theme_builder = MagicMock() + theme_builder.with_temp_folder.return_value = theme_builder + theme_builder.with_theme_name.return_value = theme_builder + theme_builder.with_mode.return_value = theme_builder + theme_builder.filled.return_value = theme_builder + theme_builder.with_logger_factory.return_value = theme_builder + theme_builder.with_reset_dependencies.return_value = theme_builder + theme_builder.build.return_value = DummyTheme() + return theme_builder + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + + def test_use_backup_as_source(self): + self.preparer.use_backup_as_source() + + self.gresource.use_backup_gresource.assert_called_once() + + @patch("os.listdir") + def test_preparer_extracts_gresource(self, mock_listdir): + mock_listdir.return_value = ["gnome-shell.css"] + + self.preparer.prepare() + + self.gresource.extract.assert_called_once() + + @patch("os.listdir") + def test_preparer_scans_correct_directory(self, mock_listdir): + mock_listdir.return_value = ["gnome-shell.css"] + + self.preparer.prepare() + + mock_listdir.assert_called_once_with(self.gresource.temp_folder) + + @patch("os.listdir") + def test_preparer_filters_valid_css_files(self, mock_listdir): + valid_files = ["gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"] + invalid_files = ["other.css", "readme.txt"] + mock_listdir.return_value = valid_files + invalid_files + + themes = self.preparer.prepare() + + self.assertEqual(len(themes), len(valid_files)) + + @patch("os.listdir") + def test_preparer_assigns_correct_labels(self, mock_listdir): + test_files = ["gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"] + mock_listdir.return_value = test_files + + themes = self.preparer.prepare() + + expected_labels = { + "gnome-shell-dark.css": "dark", + "gnome-shell-light.css": "light", + "gnome-shell.css": "light" # Uses default_mode + } + + for theme_obj in themes: + file_name = os.path.basename(theme_obj.theme_file) + self.assertEqual(theme_obj.label, expected_labels[file_name]) + + @patch("os.listdir") + def test_preparer_configures_theme_builder_correctly(self, mock_listdir): + mock_listdir.return_value = ["gnome-shell-dark.css", "gnome-shell.css"] + + self.preparer.prepare() + + self.theme_builder.with_theme_name.assert_any_call("gnome-shell-dark") + self.theme_builder.with_theme_name.assert_any_call("gnome-shell") + diff --git a/tests/utils/global_theme/test_gdm_remover.py b/tests/utils/global_theme/test_gdm_remover.py new file mode 100644 index 0000000..04b2d07 --- /dev/null +++ b/tests/utils/global_theme/test_gdm_remover.py @@ -0,0 +1,44 @@ +from unittest import TestCase +from unittest.mock import MagicMock + +from scripts.utils.global_theme.gdm_remover import GDMThemeRemover +from scripts.utils.gresource import GresourceBackupNotFoundError + + +class GDMRemoverTestCase(TestCase): + def setUp(self): + self.gresource = MagicMock() + self.alternatives_updater = MagicMock() + self.logger = MagicMock() + self.logger_factory = MagicMock(return_value=self.logger) + + self.remover = GDMThemeRemover( + gresource=self.gresource, + alternatives_updater=self.alternatives_updater, + logger_factory=self.logger_factory + ) + + self.remover.remover_logger = MagicMock() + + def test_remove_logs_start_message(self): + self.remover.remove() + + self.remover.remover_logger.start_removing.assert_called_once() + + def test_remove_calls_gresource_restore_and_alternatives_remove(self): + self.remover.remove() + + self.gresource.restore.assert_called_once() + self.alternatives_updater.remove.assert_called_once() + + def test_remove_logs_success_message(self): + self.remover.remove() + + self.remover.remover_logger.success_removing.assert_called_once() + + def test_remove_logs_error_message_when_backup_not_found(self): + self.gresource.restore.side_effect = GresourceBackupNotFoundError() + + self.remover.remove() + + self.remover.remover_logger.error_removing.assert_called_once() \ No newline at end of file diff --git a/tests/utils/global_theme/test_gdm_theme_prepare.py b/tests/utils/global_theme/test_gdm_theme_prepare.py new file mode 100644 index 0000000..cc0fac1 --- /dev/null +++ b/tests/utils/global_theme/test_gdm_theme_prepare.py @@ -0,0 +1,118 @@ +import os.path +import shutil +from unittest import TestCase +from unittest.mock import MagicMock + +from scripts import config +from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare +from ..._helpers import create_dummy_file, try_remove_file + + +class GDMThemePrepareTestCase(TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_theme_prepare") + self.main_styles = os.path.join(self.temp_folder, "gnome-shell.css") + self.theme = MagicMock() + self.theme.add_to_start.return_value = None + self.theme.temp_folder = self.temp_folder + self.theme.main_styles = self.main_styles + + self.main_styles_destination = os.path.join(self.temp_folder, "gnome-shell-result.css") + create_dummy_file(self.main_styles_destination, "body { background-color: #000; }") + + self.files_labeler = MagicMock() + + self.theme_prepare = GDMThemePrepare( + theme=self.theme, + theme_file=self.main_styles_destination, + label=None, + files_labeler=self.files_labeler, + ) + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + + def test_label_files_calls_labeler(self): + self.theme_prepare.label = "dark" + + self.theme_prepare.label_theme() + + self.files_labeler.append_label.assert_called_once_with("dark") + + def test_label_files_raises_value_error_if_label_none(self): + self.theme_prepare.label = None + + with self.assertRaises(ValueError): + self.theme_prepare.label_theme() + + def test_remove_keywords_removes_destination_keywords(self): + try_remove_file(self.main_styles_destination) + expected_content = "body { background-color: #000; }" + create_dummy_file(self.main_styles_destination, "body {keyword1 background-color: #000 !important; }") + keywords = ["keyword1", " !important"] + + self.theme_prepare.remove_keywords(*keywords) + + with open(self.main_styles_destination, 'r') as file: + content = file.read() + self.assertEqual(content, expected_content) + try_remove_file(self.main_styles_destination) + + def test_remove_properties_removes_destination_properties(self): + try_remove_file(self.main_styles_destination) + expected_content = "body {\n}\n" + create_dummy_file(self.main_styles_destination, "body {\nbackground-color: #000;\n}") + properties = ["background-color"] + + self.theme_prepare.remove_properties(*properties) + + with open(self.main_styles_destination, 'r') as file: + actual_content = file.read() + self.assertEqual(expected_content, actual_content) + try_remove_file(self.main_styles_destination) + + def test_remove_properties_removes_one_line_properties(self): + try_remove_file(self.main_styles_destination) + expected_content = "" + create_dummy_file(self.main_styles_destination, "body { background-color: #000; }") + properties = ["background-color"] + + self.theme_prepare.remove_properties(*properties) + + with open(self.main_styles_destination, 'r') as file: + actual_content = file.read() + self.assertEqual(expected_content, actual_content) + try_remove_file(self.main_styles_destination) + + def test_prepend_source_styles_prepends_destination_styles(self): + try_remove_file(self.main_styles_destination) + expected_content = "body { background-color: #000; }\n" + create_dummy_file(self.main_styles_destination, "body { background-color: #000; }") + + self.theme_prepare.prepend_source_styles("") + + called_content: str = self.theme.add_to_start.call_args[0][0] + self.assertTrue(called_content.startswith(expected_content)) + try_remove_file(self.main_styles_destination) + + def test_prepend_source_styles_adds_trigger(self): + try_remove_file(self.main_styles_destination) + expected_content = "\ntrigger\n" + create_dummy_file(self.main_styles_destination) + trigger = "trigger" + + self.theme_prepare.prepend_source_styles(trigger) + + called_content: str = self.theme.add_to_start.call_args[0][0] + self.assertTrue(expected_content in called_content) + try_remove_file(self.main_styles_destination) + + def test_install_passes_arguments_to_theme(self): + hue = 0 + color = "#000000" + sat = 100 + destination = os.path.join(self.temp_folder, "destination") + + self.theme_prepare.install(hue, color, sat, destination) + + self.theme.install.assert_called_once_with(hue, color, sat, destination=destination) \ No newline at end of file diff --git a/tests/utils/global_theme/test_ubuntu_gdm_update_alternatives.py b/tests/utils/global_theme/test_ubuntu_gdm_update_alternatives.py new file mode 100644 index 0000000..98b5f3e --- /dev/null +++ b/tests/utils/global_theme/test_ubuntu_gdm_update_alternatives.py @@ -0,0 +1,47 @@ +from unittest import TestCase +from unittest.mock import MagicMock + +from scripts.utils.alternatives_updater import AlternativesUpdater +from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater + + +class UbuntuGDMUpdateAlternativesTestCase(TestCase): + def setUp(self): + self.updater = MagicMock(spec=AlternativesUpdater) + self.ubuntu_updater = UbuntuGDMAlternativesUpdater( + alternatives_updater=self.updater + ) + + def test_custom_destination_updates_correctly(self): + custom_destination_dir = "/custom/path" + custom_destination_file = "custom_file.gresource" + + self.ubuntu_updater.with_custom_destination( + custom_destination_dir, custom_destination_file + ) + + self.assertEqual( + self.ubuntu_updater.destination_dir, custom_destination_dir + ) + self.assertEqual( + self.ubuntu_updater.destination_file, custom_destination_file + ) + + def test_install_and_set_calls_updater_correctly(self): + priority = 100 + self.ubuntu_updater.install_and_set(priority) + + self.updater.install_and_set.assert_called_once_with( + link=self.ubuntu_updater.ubuntu_gresource_path, + name=self.ubuntu_updater.ubuntu_gresource_link_name, + path=self.ubuntu_updater.gnome_gresource_path, + priority=priority + ) + + def test_remove_calls_updater_correctly(self): + self.ubuntu_updater.remove() + + self.updater.remove.assert_called_once_with( + name=self.ubuntu_updater.ubuntu_gresource_link_name, + path=self.ubuntu_updater.gnome_gresource_path + ) \ No newline at end of file diff --git a/tests/utils/test_files_labeler.py b/tests/utils/test_files_labeler.py new file mode 100644 index 0000000..8c024a1 --- /dev/null +++ b/tests/utils/test_files_labeler.py @@ -0,0 +1,57 @@ +import os.path +import shutil +from unittest import TestCase + +from scripts import config +from scripts.utils.files_labeler import FilesLabelerFactoryImpl +from .._helpers import create_dummy_file + + +class FilesLabelerTestCase(TestCase): + def setUp(self): + self.temp_folder = os.path.join(config.temp_tests_folder, "labeler") + + self.files = ["file1.svg", "file2.png", "file3.svg"] + self.styles_file = os.path.join(self.temp_folder, "styles-test.css") # styles files are already labeled + self.original_styles_content = f"body {{ background: url('./{self.files[0]}'); }}" + + self.factory = FilesLabelerFactoryImpl() + + def _generate_test_files(self): + self.tearDown() + + for filename in self.files: + create_dummy_file(os.path.join(self.temp_folder, filename)) + + create_dummy_file(self.styles_file, self.original_styles_content) + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + + def test_append_label_correctly_labels_files(self): + self._generate_test_files() + label = "test" + labeled_files = [(f, f.replace(".", f"-{label}.")) for f in self.files] + labeler = self.factory.create(self.temp_folder) + + labeler.append_label(label) + + for original, labeled in labeled_files: + labeled_path = os.path.join(self.temp_folder, labeled) + original_path = os.path.join(self.temp_folder, original) + self.assertTrue(os.path.exists(labeled_path)) + self.assertFalse(os.path.exists(original_path)) + + def test_append_label_correctly_updates_references(self): + self._generate_test_files() + label = "test" + replaced_file = self.files[0].replace('.', f'-{label}.') + expected_content = f"body {{ background: url('./{replaced_file}'); }}" + labeler = self.factory.create(self.temp_folder, self.styles_file) + + labeler.append_label(label) + + with open(self.styles_file, 'r') as file: + actual_content = file.read() + self.assertNotEqual(actual_content, self.original_styles_content) + self.assertEqual(actual_content, expected_content) \ No newline at end of file