Cover GDM module with tests, added FilesLabelerFactory, renamed GdmBuilder

This commit is contained in:
Vladyslav Hroshev
2025-04-13 23:50:22 +03:00
parent ca4e4d4cbe
commit 58658ff7fc
17 changed files with 775 additions and 31 deletions

View File

@@ -1,6 +1,6 @@
# This file installs Marble shell theme for GNOME DE # This file installs Marble shell theme for GNOME DE
# Copyright (C) 2023-2025 Vladyslav Hroshev # Copyright (C) 2023-2025 Vladyslav Hroshev
import os
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # 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.global_theme_installer import GlobalThemeInstaller
from scripts.install.local_theme_installer import LocalThemeInstaller from scripts.install.local_theme_installer import LocalThemeInstaller
from scripts.utils.gnome import apply_gnome_theme from scripts.utils.gnome import apply_gnome_theme
from scripts.utils.logger.console import Console
def main(): def main():
@@ -31,6 +32,12 @@ def main():
installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller
installer = installer_class(args, colors_definer) 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: if args.remove or args.reinstall:
installer.remove() installer.remove()

View File

@@ -1,6 +1,6 @@
from scripts.install.theme_installer import ThemeInstaller from scripts.install.theme_installer import ThemeInstaller
from scripts.utils.global_theme.gdm import GDMTheme 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 from scripts.utils.logger.console import Console, Color, Format
@@ -13,7 +13,7 @@ class GlobalThemeInstaller(ThemeInstaller):
print("GDM theme removed successfully.") print("GDM theme removed successfully.")
def _define_theme(self): def _define_theme(self):
gdm_builder = GdmBuilder(self.colors) gdm_builder = GDMThemeBuilder(self.colors)
gdm_builder.with_mode(self.args.mode) gdm_builder.with_mode(self.args.mode)
gdm_builder.with_filled(self.args.filled) gdm_builder.with_filled(self.args.filled)
self.theme = gdm_builder.build() self.theme = gdm_builder.build()

View File

@@ -1,15 +1,28 @@
import os import os
from abc import ABC, abstractmethod
from typing import Tuple, TypeAlias from typing import Tuple, TypeAlias
LabeledFileGroup: TypeAlias = Tuple[str, str] 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: 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 Initialize the working directory and files to change
""" """
self.directory = directory self.directory = directory
self.files = args self.files = files_to_update_references
def append_label(self, label: str): def append_label(self, label: str):
""" """

View File

@@ -6,6 +6,7 @@ from scripts.install.colors_definer import ColorsDefiner
from scripts.types.installation_color import InstallationMode from scripts.types.installation_color import InstallationMode
from scripts.utils.alternatives_updater import AlternativesUpdater, PathString from scripts.utils.alternatives_updater import AlternativesUpdater, PathString
from scripts.utils.command_runner.subprocess_command_runner import SubprocessCommandRunner 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 import GDMTheme
from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller
from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer 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 from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
class GdmBuilder: class GDMThemeBuilder:
""" """
Builder class for creating GDMTheme instances with configurable components. Builder class for creating GDMTheme instances with configurable components.
@@ -26,7 +27,7 @@ class GdmBuilder:
automatically resolved during build() if not provided. automatically resolved during build() if not provided.
Example usage: Example usage:
builder = GdmBuilder(colors_provider) builder = GMDThemeBuilder(colors_provider)
theme = builder.with_mode("dark").with_filled(True).build() theme = builder.with_mode("dark").with_filled(True).build()
""" """
def __init__(self, colors_provider: ColorsDefiner): def __init__(self, colors_provider: ColorsDefiner):
@@ -46,42 +47,42 @@ class GdmBuilder:
self._installer: Optional[GDMThemeInstaller] = None self._installer: Optional[GDMThemeInstaller] = None
self._remover: Optional[GDMThemeRemover] = 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.""" """Set the mode for the theme."""
self._mode = mode self._mode = mode
return self 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.""" """Set the filled state for the theme."""
self._is_filled = is_filled self._is_filled = is_filled
return self 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.""" """Inject a logger factory for logging purposes."""
self._logger_factory = logger_factory self._logger_factory = logger_factory
return self return self
def with_gresource(self, gresource: Gresource) -> 'GdmBuilder': def with_gresource(self, gresource: Gresource) -> 'GDMThemeBuilder':
"""Inject a gresource instance for managing gresource files.""" """Inject a gresource instance for managing gresource files."""
self._gresource = gresource self._gresource = gresource
return self 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.""" """Inject an alternatives updater for managing GDM alternatives."""
self._ubuntu_gdm_alternatives_updater = alternatives_updater self._ubuntu_gdm_alternatives_updater = alternatives_updater
return self return self
def with_preparer(self, preparer: GDMThemePreparer) -> 'GdmBuilder': def with_preparer(self, preparer: GDMThemePreparer) -> 'GDMThemeBuilder':
"""Inject a preparer for preparing the theme.""" """Inject a preparer for preparing the theme."""
self._preparer = preparer self._preparer = preparer
return self return self
def with_installer(self, installer: GDMThemeInstaller) -> 'GdmBuilder': def with_installer(self, installer: GDMThemeInstaller) -> 'GDMThemeBuilder':
"""Inject an installer for installing the theme.""" """Inject an installer for installing the theme."""
self._installer = installer self._installer = installer
return self return self
def with_remover(self, remover: GDMThemeRemover) -> 'GdmBuilder': def with_remover(self, remover: GDMThemeRemover) -> 'GDMThemeBuilder':
"""Inject a remover for removing the theme.""" """Inject a remover for removing the theme."""
self._remover = remover self._remover = remover
return self return self
@@ -141,13 +142,15 @@ class GdmBuilder:
"""Create a GDMThemePreparer if not explicitly provided.""" """Create a GDMThemePreparer if not explicitly provided."""
if self._preparer: return if self._preparer: return
theme_builder = GnomeShellThemeBuilder(self.colors_provider) theme_builder = GnomeShellThemeBuilder(self.colors_provider)
files_labeler_factory = FilesLabelerFactoryImpl()
self._preparer = GDMThemePreparer( self._preparer = GDMThemePreparer(
temp_folder=self._temp_folder, temp_folder=self._temp_folder,
default_mode=self._mode, default_mode=self._mode,
is_filled=self._is_filled, is_filled=self._is_filled,
gresource=self._gresource, gresource=self._gresource,
theme_builder=theme_builder, theme_builder=theme_builder,
logger_factory=self._logger_factory logger_factory=self._logger_factory,
files_labeler_factory=files_labeler_factory,
) )
def _resolve_installer(self): def _resolve_installer(self):

View File

@@ -1,5 +1,6 @@
import os import os
from scripts.utils.files_labeler import FilesLabelerFactory
from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare
from scripts.utils.gresource.gresource import Gresource from scripts.utils.gresource.gresource import Gresource
from scripts.utils.logger.logger import LoggerFactory 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, def __init__(self, temp_folder: str, default_mode: str | None, is_filled: bool,
gresource: Gresource, gresource: Gresource,
theme_builder: GnomeShellThemeBuilder, theme_builder: GnomeShellThemeBuilder,
logger_factory: LoggerFactory): logger_factory: LoggerFactory,
files_labeler_factory: FilesLabelerFactory):
""" """
:param temp_folder: Temporary folder for extracted theme files :param temp_folder: Temporary folder for extracted theme files
:param default_mode: Default theme mode to use if not specified in CSS filename :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 gresource: Gresource instance for managing gresource files
:param theme_builder: Theme builder instance for creating themes :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.temp_folder = temp_folder
self.gresource_temp_folder = gresource.temp_folder self.gresource_temp_folder = gresource.temp_folder
@@ -36,6 +40,7 @@ class GDMThemePreparer:
self.gresource = gresource self.gresource = gresource
self.theme_builder = theme_builder self.theme_builder = theme_builder
self.logger_factory = logger_factory self.logger_factory = logger_factory
self.files_labeler_factory = files_labeler_factory
def use_backup_as_source(self): def use_backup_as_source(self):
"""Use backup gresource file for extraction""" """Use backup gresource file for extraction"""
@@ -72,11 +77,12 @@ class GDMThemePreparer:
theme.prepare() theme.prepare()
theme_file = os.path.join(self.gresource_temp_folder, file_name) 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): 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", "") theme_name = file_name.replace(".css", "")
(self.theme_builder (self.theme_builder

View File

@@ -12,15 +12,18 @@ class GDMThemePrepare:
- CSS property and keyword removal for customization - CSS property and keyword removal for customization
- Theme installation with color adjustments - 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: The theme object to prepare
:param theme_file: Path to the original decompiled CSS file :param theme_file: Path to the original decompiled CSS file
:param label: Optional label for the theme (e.g. "dark", "light") :param label: Optional label for the theme (e.g. "dark", "light")
:param files_labeler: FilesLabeler instance for labeling files
""" """
self.theme = theme self.theme = theme
self.theme_file = theme_file self.theme_file = theme_file
self.label = label self.label = label
self.files_labeler = files_labeler
def label_theme(self): def label_theme(self):
""" """
@@ -31,8 +34,7 @@ class GDMThemePrepare:
if self.label is None: if self.label is None:
raise ValueError("Label is not set for the theme.") raise ValueError("Label is not set for the theme.")
files_labeler = FilesLabeler(self.theme.temp_folder, self.theme.main_styles) self.files_labeler.append_label(self.label)
files_labeler.append_label(self.label)
def remove_keywords(self, *args: str): def remove_keywords(self, *args: str):
"""Remove specific keywords from the theme file""" """Remove specific keywords from the theme file"""
@@ -52,7 +54,7 @@ class GDMThemePrepare:
:param trigger: String marker used to identify installed themes :param trigger: String marker used to identify installed themes
""" """
with open(self.theme_file, 'r') as gnome_theme: 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) self.theme.add_to_start(gnome_styles)
def install(self, hue: int, color: str, sat: int | None, destination: str): def install(self, hue: int, color: str, sat: int | None, destination: str):

View File

@@ -17,7 +17,7 @@ class UbuntuGDMAlternativesUpdater:
""" """
:param alternatives_updater: Handler for update-alternatives operations :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_dir = config.global_gnome_shell_theme
self.destination_file = config.gnome_shell_gresource self.destination_file = config.gnome_shell_gresource
@@ -26,7 +26,7 @@ class UbuntuGDMAlternativesUpdater:
self._update_gresource_paths() self._update_gresource_paths()
def _update_gresource_paths(self): 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) self.gnome_gresource_path = os.path.join(self.destination_dir, self.destination_file)
def with_custom_destination(self, destination_dir: str, destination_file: str): def with_custom_destination(self, destination_dir: str, destination_file: str):
@@ -47,7 +47,7 @@ class UbuntuGDMAlternativesUpdater:
""" """
self.alternatives_updater.install_and_set( self.alternatives_updater.install_and_set(
link=self.ubuntu_gresource_path, link=self.ubuntu_gresource_path,
name=self.ubuntu_gresource_link, name=self.ubuntu_gresource_link_name,
path=self.gnome_gresource_path, path=self.gnome_gresource_path,
priority=priority priority=priority
) )
@@ -60,6 +60,6 @@ class UbuntuGDMAlternativesUpdater:
the system to fall back to the default GDM theme. the system to fall back to the default GDM theme.
""" """
self.alternatives_updater.remove( self.alternatives_updater.remove(
name=self.ubuntu_gresource_link, name=self.ubuntu_gresource_link_name,
path=self.gnome_gresource_path path=self.gnome_gresource_path
) )

View File

@@ -10,10 +10,10 @@ def remove_properties(file, *args):
with open(file, "r") as read_file: with open(file, "r") as read_file:
content = read_file.read() 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): if not any(prop in line for prop in args):
new_content += line + "\n" new_content += line + "\n"
elif "}" in line: elif "}" in line and not "{" in line:
new_content += "}\n" new_content += "}\n"
with open(file, "w") as write_file: with open(file, "w") as write_file:

View File

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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)