Attempt to speed up Steam even more. Sound for link added.
This commit is contained in:
@@ -10,6 +10,8 @@ default_theme_sounds = files(
|
|||||||
'default/checkbox_unchecked.wav',
|
'default/checkbox_unchecked.wav',
|
||||||
'default/combobox.wav',
|
'default/combobox.wav',
|
||||||
'default/focus_mode.wav',
|
'default/focus_mode.wav',
|
||||||
|
'default/link.wav',
|
||||||
|
'default/link_visited.wav',
|
||||||
'default/radiobutton_checked.wav',
|
'default/radiobutton_checked.wav',
|
||||||
'default/radiobutton_unchecked.wav',
|
'default/radiobutton_unchecked.wav',
|
||||||
'default/start.wav',
|
'default/start.wav',
|
||||||
|
|||||||
@@ -914,6 +914,37 @@ class PluginSystemManager:
|
|||||||
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
|
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
|
||||||
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
|
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lifecycle_accepts_plugin_instance(method: Any) -> bool:
|
||||||
|
"""Return True if the bound lifecycle method expects a plugin argument."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
parameters = list(inspect.signature(method).parameters.values())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if any(parameter.kind == inspect.Parameter.VAR_POSITIONAL for parameter in parameters):
|
||||||
|
return True
|
||||||
|
|
||||||
|
positional_parameters = [
|
||||||
|
parameter for parameter in parameters
|
||||||
|
if parameter.kind in (
|
||||||
|
inspect.Parameter.POSITIONAL_ONLY,
|
||||||
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return bool(positional_parameters)
|
||||||
|
|
||||||
|
def _callPluginLifecycle(self, pluginInstance: Any, methodName: str) -> None:
|
||||||
|
"""Call a plugin lifecycle method without replaying it through pluggy."""
|
||||||
|
|
||||||
|
method = getattr(pluginInstance, methodName)
|
||||||
|
if self._lifecycle_accepts_plugin_instance(method):
|
||||||
|
method(pluginInstance)
|
||||||
|
return
|
||||||
|
|
||||||
|
method()
|
||||||
|
|
||||||
def loadPlugin(self, pluginInfo):
|
def loadPlugin(self, pluginInfo):
|
||||||
"""Load a plugin."""
|
"""Load a plugin."""
|
||||||
module_name = pluginInfo.get_module_name()
|
module_name = pluginInfo.get_module_name()
|
||||||
@@ -998,7 +1029,7 @@ class PluginSystemManager:
|
|||||||
logger.info(f"Activating plugin: {module_name}")
|
logger.info(f"Activating plugin: {module_name}")
|
||||||
# Lifecycle is per-plugin. Broadcasting through pluggy replays
|
# Lifecycle is per-plugin. Broadcasting through pluggy replays
|
||||||
# activate() on every previously-registered plugin.
|
# activate() on every previously-registered plugin.
|
||||||
plugin_instance.activate(plugin_instance)
|
self._callPluginLifecycle(plugin_instance, "activate")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error activating plugin {module_name}: {e}")
|
logger.error(f"Error activating plugin {module_name}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
@@ -1039,7 +1070,7 @@ class PluginSystemManager:
|
|||||||
try:
|
try:
|
||||||
# Mirror targeted activation and only deactivate the plugin
|
# Mirror targeted activation and only deactivate the plugin
|
||||||
# instance being unloaded.
|
# instance being unloaded.
|
||||||
plugin_instance.deactivate(plugin_instance)
|
self._callPluginLifecycle(plugin_instance, "deactivate")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deactivating plugin {module_name}: {e}")
|
logger.error(f"Error deactivating plugin {module_name}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import gi
|
|||||||
import locale
|
import locale
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
from typing import Any, Callable, Generator, Optional, TYPE_CHECKING
|
from typing import Any, Callable, Generator, Optional, TYPE_CHECKING
|
||||||
@@ -200,7 +199,8 @@ class Utilities:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmdline = subprocess.getoutput(f"cat /proc/{pid}/cmdline")
|
with open(f"/proc/{pid}/cmdline", "rb") as cmdline_file:
|
||||||
|
cmdline = cmdline_file.read().decode("utf-8", errors="replace")
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -211,7 +211,9 @@ class Utilities:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
app = AXObject.get_application(window)
|
app = AXObject.get_application(window)
|
||||||
tokens = ["SCRIPT UTILITIES: Looking at", window, "from", app, self._getAppCommandLine(app)]
|
tokens = ["SCRIPT UTILITIES: Looking at", window, "from", app]
|
||||||
|
if debug.debugLevel <= debug.LEVEL_INFO:
|
||||||
|
tokens.append(self._getAppCommandLine(app))
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
if clearCache:
|
if clearCache:
|
||||||
@@ -3082,6 +3084,56 @@ class Utilities:
|
|||||||
|
|
||||||
return "".join(adjustedLine)
|
return "".join(adjustedLine)
|
||||||
|
|
||||||
|
def getLinkIndicatorPresentation(self, obj: Atspi.Accessible, line: Any, startOffset: Any):
|
||||||
|
"""Return the spoken text and any link icons for a line fragment."""
|
||||||
|
|
||||||
|
spoken = self.adjustForLinks(obj, line, startOffset)
|
||||||
|
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
|
||||||
|
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY:
|
||||||
|
return spoken, []
|
||||||
|
|
||||||
|
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'):
|
||||||
|
return spoken, []
|
||||||
|
|
||||||
|
if not AXObject.supports_hypertext(obj):
|
||||||
|
return spoken, []
|
||||||
|
|
||||||
|
from . import sound_theme_manager
|
||||||
|
|
||||||
|
manager = sound_theme_manager.getManager()
|
||||||
|
if manager is None:
|
||||||
|
return spoken, []
|
||||||
|
|
||||||
|
endOffset = startOffset + len(line)
|
||||||
|
icons = []
|
||||||
|
missingIcon = False
|
||||||
|
for link in AXHypertext.get_all_links(obj):
|
||||||
|
start_index = AXHypertext.get_link_start_offset(link)
|
||||||
|
end_index = AXHypertext.get_link_end_offset(link)
|
||||||
|
if start_index < 0 or end_index < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if startOffset < end_index <= endOffset:
|
||||||
|
pass
|
||||||
|
elif startOffset <= start_index < endOffset:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
icon = manager.getLinkSoundIcon(visited=AXUtilities.is_visited(link))
|
||||||
|
if icon:
|
||||||
|
icons.append(icon)
|
||||||
|
else:
|
||||||
|
missingIcon = True
|
||||||
|
|
||||||
|
if not icons:
|
||||||
|
return spoken, []
|
||||||
|
|
||||||
|
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY and not missingIcon:
|
||||||
|
return line, icons
|
||||||
|
|
||||||
|
return spoken, icons
|
||||||
|
|
||||||
def _processMultiCaseString(self, string: Any) -> Any:
|
def _processMultiCaseString(self, string: Any) -> Any:
|
||||||
return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
|
return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
|
||||||
|
|
||||||
|
|||||||
@@ -2402,7 +2402,7 @@ class Script(script.Script):
|
|||||||
|
|
||||||
voice = self.speechGenerator.voice(
|
voice = self.speechGenerator.voice(
|
||||||
obj=obj, string=string, language=language, dialect=dialect)
|
obj=obj, string=string, language=language, dialect=dialect)
|
||||||
string = self.utilities.adjustForLinks(obj, string, start)
|
string, linkIcons = self.utilities.getLinkIndicatorPresentation(obj, string, start)
|
||||||
string = self.utilities.adjustForRepeats(string)
|
string = self.utilities.adjustForRepeats(string)
|
||||||
if self.utilities.shouldVerbalizeAllPunctuation(obj):
|
if self.utilities.shouldVerbalizeAllPunctuation(obj):
|
||||||
string = self.utilities.verbalizeAllPunctuation(string)
|
string = self.utilities.verbalizeAllPunctuation(string)
|
||||||
@@ -2414,6 +2414,7 @@ class Script(script.Script):
|
|||||||
|
|
||||||
result = [string]
|
result = [string]
|
||||||
result.extend(voice)
|
result.extend(voice)
|
||||||
|
result.extend(linkIcons)
|
||||||
utterance.append(result)
|
utterance.append(result)
|
||||||
speech.speak(utterance)
|
speech.speak(utterance)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
|||||||
roleSoundIcon = None
|
roleSoundIcon = None
|
||||||
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
|
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
|
||||||
and soundEnabled:
|
and soundEnabled:
|
||||||
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
|
roleSoundIcon = self._getRoleSoundIcon(obj, role)
|
||||||
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
|
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
|
||||||
and soundEnabled:
|
and soundEnabled:
|
||||||
# Stateful controls present their role via state sounds; suppress
|
# Stateful controls present their role via state sounds; suppress
|
||||||
|
|||||||
@@ -303,6 +303,24 @@ class SoundThemeManager:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getLinkSoundIcon(self, visited=False, themeName=None):
|
||||||
|
"""Return an Icon for a plain or visited link sound from the current theme."""
|
||||||
|
|
||||||
|
themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default'
|
||||||
|
if themeName == THEME_NONE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = ["link"]
|
||||||
|
if visited:
|
||||||
|
candidates.insert(0, "link_visited")
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
soundPath = self.getSoundPath(themeName, candidate)
|
||||||
|
if soundPath:
|
||||||
|
return Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def getRoleStateSoundIcon(self, role, stateKey, themeName=None):
|
def getRoleStateSoundIcon(self, role, stateKey, themeName=None):
|
||||||
"""Return an Icon for the role/state sound from the current theme, if any."""
|
"""Return an Icon for the role/state sound from the current theme, if any."""
|
||||||
themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default'
|
themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default'
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ class SpeechGenerator(generator.Generator):
|
|||||||
def __init__(self, script):
|
def __init__(self, script):
|
||||||
generator.Generator.__init__(self, script, "speech")
|
generator.Generator.__init__(self, script, "speech")
|
||||||
|
|
||||||
|
def _getRoleSoundIcon(self, obj, role):
|
||||||
|
"""Return the themed sound icon for obj's role, if any."""
|
||||||
|
|
||||||
|
if role == Atspi.Role.LINK or AXUtilities.is_link(obj):
|
||||||
|
return sound_theme_manager.getManager().getLinkSoundIcon(
|
||||||
|
visited=AXUtilities.is_visited(obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
return sound_theme_manager.getManager().getRoleSoundIcon(role)
|
||||||
|
|
||||||
def _addGlobals(self, globalsDict):
|
def _addGlobals(self, globalsDict):
|
||||||
"""Other things to make available from the formatting string.
|
"""Other things to make available from the formatting string.
|
||||||
"""
|
"""
|
||||||
@@ -676,7 +686,7 @@ class SpeechGenerator(generator.Generator):
|
|||||||
roleSoundIcon = None
|
roleSoundIcon = None
|
||||||
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
|
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
|
||||||
and soundEnabled:
|
and soundEnabled:
|
||||||
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
|
roleSoundIcon = self._getRoleSoundIcon(obj, role)
|
||||||
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
|
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
|
||||||
and soundEnabled:
|
and soundEnabled:
|
||||||
# Stateful controls present their role via state sounds; suppress
|
# Stateful controls present their role via state sounds; suppress
|
||||||
@@ -1442,9 +1452,10 @@ class SpeechGenerator(generator.Generator):
|
|||||||
args.pop("string")
|
args.pop("string")
|
||||||
|
|
||||||
voice = self.voice(string=string, obj=obj, **args)
|
voice = self.voice(string=string, obj=obj, **args)
|
||||||
string = self._script.utilities.adjustForLinks(obj, string, start)
|
string, linkIcons = self._script.utilities.getLinkIndicatorPresentation(obj, string, start)
|
||||||
rv = [self._script.utilities.adjustForRepeats(string)]
|
rv = [self._script.utilities.adjustForRepeats(string)]
|
||||||
rv.extend(voice)
|
rv.extend(voice)
|
||||||
|
rv.extend(linkIcons)
|
||||||
|
|
||||||
# TODO - JD: speech.speak() has a bug which causes a list of utterances to
|
# TODO - JD: speech.speak() has a bug which causes a list of utterances to
|
||||||
# be presented before a string+voice pair that comes first. Until we can
|
# be presented before a string+voice pair that comes first. Until we can
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
|
||||||
|
sound_stub = types.ModuleType("cthulhu.sound")
|
||||||
|
sound_stub.getPlayer = mock.Mock(return_value=None)
|
||||||
|
sys.modules.setdefault("cthulhu.sound", sound_stub)
|
||||||
|
|
||||||
|
speech_stub = types.ModuleType("cthulhu.speech")
|
||||||
|
speech_stub.speak = mock.Mock()
|
||||||
|
sys.modules.setdefault("cthulhu.speech", speech_stub)
|
||||||
|
|
||||||
|
from cthulhu import settings
|
||||||
|
from cthulhu.sound_theme_manager import SoundThemeManager
|
||||||
|
from cthulhu.speech_generator import SpeechGenerator
|
||||||
|
from cthulhu.script_utilities import Utilities
|
||||||
|
|
||||||
|
|
||||||
|
class LinkSoundThemeRegressionTests(unittest.TestCase):
|
||||||
|
def test_default_theme_install_manifest_includes_link_sounds(self):
|
||||||
|
mesonPath = Path(__file__).resolve().parents[1] / "sounds" / "meson.build"
|
||||||
|
mesonText = mesonPath.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("'default/link.wav'", mesonText)
|
||||||
|
self.assertIn("'default/link_visited.wav'", mesonText)
|
||||||
|
|
||||||
|
def _create_manager(self):
|
||||||
|
app = mock.Mock()
|
||||||
|
settingsManager = mock.Mock()
|
||||||
|
settingsManager.getSetting.return_value = "default"
|
||||||
|
app.getSettingsManager.return_value = settingsManager
|
||||||
|
return SoundThemeManager(app)
|
||||||
|
|
||||||
|
def test_visited_link_prefers_visited_asset(self):
|
||||||
|
manager = self._create_manager()
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
manager,
|
||||||
|
"getSoundPath",
|
||||||
|
side_effect=["/tmp/link_visited.wav", "/tmp/link.wav"],
|
||||||
|
):
|
||||||
|
icon = manager.getLinkSoundIcon(visited=True)
|
||||||
|
|
||||||
|
self.assertTrue(icon.path.endswith("link_visited.wav"))
|
||||||
|
|
||||||
|
def test_visited_link_falls_back_to_plain_link_asset(self):
|
||||||
|
manager = self._create_manager()
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
manager,
|
||||||
|
"getSoundPath",
|
||||||
|
side_effect=[None, "/tmp/link.wav"],
|
||||||
|
):
|
||||||
|
icon = manager.getLinkSoundIcon(visited=True)
|
||||||
|
|
||||||
|
self.assertTrue(icon.path.endswith("link.wav"))
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechGeneratorLinkSoundRegressionTests(unittest.TestCase):
|
||||||
|
def test_role_sound_icon_uses_visited_link_asset_for_visited_links(self):
|
||||||
|
generator = object.__new__(SpeechGenerator)
|
||||||
|
generator._script = mock.Mock()
|
||||||
|
generator._script.utilities = mock.Mock()
|
||||||
|
|
||||||
|
icon = SimpleNamespace(path="/tmp/link_visited.wav")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("cthulhu.speech_generator.AXUtilities.is_link", return_value=True),
|
||||||
|
mock.patch("cthulhu.speech_generator.AXUtilities.is_visited", return_value=True),
|
||||||
|
mock.patch("cthulhu.speech_generator.sound_theme_manager.getManager") as get_manager,
|
||||||
|
):
|
||||||
|
get_manager.return_value.getLinkSoundIcon.return_value = icon
|
||||||
|
result = generator._getRoleSoundIcon(mock.Mock(), "link-role")
|
||||||
|
|
||||||
|
self.assertEqual(result, icon)
|
||||||
|
get_manager.return_value.getLinkSoundIcon.assert_called_once_with(visited=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LinkIndicatorPresentationRegressionTests(unittest.TestCase):
|
||||||
|
def test_sound_only_inline_link_marker_uses_icon_instead_of_word(self):
|
||||||
|
utility = object.__new__(Utilities)
|
||||||
|
utility.adjustForLinks = mock.Mock(return_value="Docs link")
|
||||||
|
|
||||||
|
icon = SimpleNamespace(path="/tmp/link.wav")
|
||||||
|
link = mock.Mock()
|
||||||
|
|
||||||
|
def get_setting(name):
|
||||||
|
values = {
|
||||||
|
"roleSoundPresentation": settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY,
|
||||||
|
"enableSound": True,
|
||||||
|
}
|
||||||
|
return values.get(name)
|
||||||
|
|
||||||
|
fakeApp = mock.Mock()
|
||||||
|
fakeApp.settingsManager.getSetting.side_effect = get_setting
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("cthulhu.script_utilities.AXObject.supports_hypertext", return_value=True),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_all_links", return_value=[link]),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_link_start_offset", return_value=0),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_link_end_offset", return_value=4),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXUtilities.is_visited", return_value=False),
|
||||||
|
mock.patch("cthulhu.sound_theme_manager.getManager") as get_manager,
|
||||||
|
mock.patch("cthulhu.script_utilities.cthulhu.cthulhuApp", fakeApp),
|
||||||
|
):
|
||||||
|
get_manager.return_value.getLinkSoundIcon.return_value = icon
|
||||||
|
spoken, icons = utility.getLinkIndicatorPresentation(mock.Mock(), "Docs", 0)
|
||||||
|
|
||||||
|
self.assertEqual(spoken, "Docs")
|
||||||
|
self.assertEqual(icons, [icon])
|
||||||
|
|
||||||
|
def test_sound_and_speech_inline_link_marker_keeps_word_and_adds_icon(self):
|
||||||
|
utility = object.__new__(Utilities)
|
||||||
|
utility.adjustForLinks = mock.Mock(return_value="Docs link")
|
||||||
|
|
||||||
|
icon = SimpleNamespace(path="/tmp/link_visited.wav")
|
||||||
|
link = mock.Mock()
|
||||||
|
|
||||||
|
def get_setting(name):
|
||||||
|
values = {
|
||||||
|
"roleSoundPresentation": settings.ROLE_SOUND_PRESENTATION_SOUND_AND_SPEECH,
|
||||||
|
"enableSound": True,
|
||||||
|
}
|
||||||
|
return values.get(name)
|
||||||
|
|
||||||
|
fakeApp = mock.Mock()
|
||||||
|
fakeApp.settingsManager.getSetting.side_effect = get_setting
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("cthulhu.script_utilities.AXObject.supports_hypertext", return_value=True),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_all_links", return_value=[link]),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_link_start_offset", return_value=0),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXHypertext.get_link_end_offset", return_value=4),
|
||||||
|
mock.patch("cthulhu.script_utilities.AXUtilities.is_visited", return_value=True),
|
||||||
|
mock.patch("cthulhu.sound_theme_manager.getManager") as get_manager,
|
||||||
|
mock.patch("cthulhu.script_utilities.cthulhu.cthulhuApp", fakeApp),
|
||||||
|
):
|
||||||
|
get_manager.return_value.getLinkSoundIcon.return_value = icon
|
||||||
|
spoken, icons = utility.getLinkIndicatorPresentation(mock.Mock(), "Docs", 0)
|
||||||
|
|
||||||
|
self.assertEqual(spoken, "Docs link")
|
||||||
|
self.assertEqual(icons, [icon])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -34,13 +34,37 @@ class {class_name}(Plugin):
|
|||||||
self.deactivation_count += 1
|
self.deactivation_count += 1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
PLUGIN_TEMPLATE_PLAIN_SIGNATURE = """
|
||||||
|
from cthulhu.plugin import Plugin, cthulhu_hookimpl
|
||||||
|
|
||||||
|
|
||||||
|
class {class_name}(Plugin):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.activation_count = 0
|
||||||
|
self.deactivation_count = 0
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def activate(self):
|
||||||
|
self.activation_count += 1
|
||||||
|
|
||||||
|
@cthulhu_hookimpl
|
||||||
|
def deactivate(self):
|
||||||
|
self.deactivation_count += 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PluginSystemManagerRegressionTests(unittest.TestCase):
|
class PluginSystemManagerRegressionTests(unittest.TestCase):
|
||||||
def _create_plugin_info(self, root_dir: str, module_name: str) -> PluginInfo:
|
def _create_plugin_info(
|
||||||
|
self,
|
||||||
|
root_dir: str,
|
||||||
|
module_name: str,
|
||||||
|
template: str = PLUGIN_TEMPLATE,
|
||||||
|
) -> PluginInfo:
|
||||||
plugin_dir = Path(root_dir) / module_name
|
plugin_dir = Path(root_dir) / module_name
|
||||||
plugin_dir.mkdir()
|
plugin_dir.mkdir()
|
||||||
(plugin_dir / "plugin.py").write_text(
|
(plugin_dir / "plugin.py").write_text(
|
||||||
textwrap.dedent(PLUGIN_TEMPLATE.format(class_name=module_name)),
|
textwrap.dedent(template.format(class_name=module_name)),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
return PluginInfo(
|
return PluginInfo(
|
||||||
@@ -93,6 +117,26 @@ class PluginSystemManagerRegressionTests(unittest.TestCase):
|
|||||||
self.assertEqual(first_plugin.instance.deactivation_count, 0)
|
self.assertEqual(first_plugin.instance.deactivation_count, 0)
|
||||||
self.assertEqual(second_instance.deactivation_count, 1)
|
self.assertEqual(second_instance.deactivation_count, 1)
|
||||||
|
|
||||||
|
@mock.patch("cthulhu.plugin_system_manager.dbus_service.get_remote_controller")
|
||||||
|
def test_loading_plugin_with_plain_activate_signature_succeeds(self, remote_controller):
|
||||||
|
remote_controller.return_value = mock.Mock()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
manager = self._create_manager()
|
||||||
|
plugin_info = self._create_plugin_info(
|
||||||
|
temp_dir,
|
||||||
|
"PlainSignaturePlugin",
|
||||||
|
template=PLUGIN_TEMPLATE_PLAIN_SIGNATURE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(manager.loadPlugin(plugin_info))
|
||||||
|
instance = plugin_info.instance
|
||||||
|
self.assertEqual(instance.activation_count, 1)
|
||||||
|
|
||||||
|
self.assertTrue(manager.unloadPlugin(plugin_info))
|
||||||
|
self.assertEqual(instance.deactivation_count, 1)
|
||||||
|
self.assertEqual(plugin_info.instance, None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
|
||||||
|
from cthulhu import debug
|
||||||
|
from cthulhu.script_utilities import Utilities
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptUtilitiesRegressionTests(unittest.TestCase):
|
||||||
|
@mock.patch("cthulhu.script_utilities.AXObject.get_application")
|
||||||
|
def test_can_be_active_window_skips_cmdline_lookup_without_info_logging(self, get_application):
|
||||||
|
utility = object.__new__(Utilities)
|
||||||
|
utility._getAppCommandLine = mock.Mock(side_effect=AssertionError("cmdline lookup should be skipped"))
|
||||||
|
utility._isActiveAndShowingAndNotIconified = mock.Mock(return_value=True)
|
||||||
|
|
||||||
|
window = object()
|
||||||
|
app = object()
|
||||||
|
get_application.return_value = app
|
||||||
|
|
||||||
|
with mock.patch.object(debug, "debugLevel", debug.LEVEL_SEVERE):
|
||||||
|
self.assertTrue(utility.canBeActiveWindow(window))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user