Attempt to make the sound system more robust. Improve some web component detection. Fixed a place where Cthulhu gets stuck on netgear's web interface.
This commit is contained in:
71
tests/test_sound_helper_backend.py
Normal file
71
tests/test_sound_helper_backend.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from cthulhu import settings
|
||||
from cthulhu import sound_helper
|
||||
|
||||
|
||||
class _FakePlaybin:
|
||||
def __init__(self):
|
||||
self.properties = {}
|
||||
|
||||
def set_property(self, name, value):
|
||||
self.properties[name] = value
|
||||
|
||||
|
||||
class SoundHelperBackendTests(unittest.TestCase):
|
||||
def test_create_file_player_uses_playbin_default_sink_for_auto(self):
|
||||
fakePlaybin = _FakePlaybin()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
sound_helper.Gst.ElementFactory,
|
||||
"make",
|
||||
return_value=fakePlaybin,
|
||||
) as makeElement,
|
||||
mock.patch.object(
|
||||
sound_helper.sound_sink,
|
||||
"create_audio_sink",
|
||||
) as createAudioSink,
|
||||
):
|
||||
player, sinkName, error = sound_helper._create_file_player("worker-file", settings.SOUND_SINK_AUTO)
|
||||
|
||||
self.assertIs(player, fakePlaybin)
|
||||
self.assertEqual(sinkName, "playbin-default")
|
||||
self.assertIsNone(error)
|
||||
createAudioSink.assert_not_called()
|
||||
makeElement.assert_called_once_with("playbin", "worker-file")
|
||||
self.assertNotIn("audio-sink", fakePlaybin.properties)
|
||||
|
||||
def test_create_file_player_sets_explicit_sink_when_requested(self):
|
||||
fakePlaybin = _FakePlaybin()
|
||||
fakeSink = object()
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
sound_helper.Gst.ElementFactory,
|
||||
"make",
|
||||
return_value=fakePlaybin,
|
||||
) as makeElement,
|
||||
mock.patch.object(
|
||||
sound_helper.sound_sink,
|
||||
"create_audio_sink",
|
||||
return_value=(fakeSink, "pulsesink", None),
|
||||
) as createAudioSink,
|
||||
):
|
||||
player, sinkName, error = sound_helper._create_file_player("worker-file", settings.SOUND_SINK_PULSE)
|
||||
|
||||
self.assertIs(player, fakePlaybin)
|
||||
self.assertEqual(sinkName, "pulsesink")
|
||||
self.assertIsNone(error)
|
||||
makeElement.assert_called_once_with("playbin", "worker-file")
|
||||
createAudioSink.assert_called_once()
|
||||
self.assertIs(fakePlaybin.properties["audio-sink"], fakeSink)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
93
tests/test_sound_recovery.py
Normal file
93
tests/test_sound_recovery.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
soundGeneratorStub = types.ModuleType("cthulhu.sound_generator")
|
||||
|
||||
|
||||
class _Icon:
|
||||
def __init__(self, path="", name=""):
|
||||
self.path = path
|
||||
self.name = name
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
||||
|
||||
class _Tone:
|
||||
def __init__(self, duration=0.1, frequency=440, volume=1.0, wave=0):
|
||||
self.duration = duration
|
||||
self.frequency = frequency
|
||||
self.volume = volume
|
||||
self.wave = wave
|
||||
|
||||
|
||||
soundGeneratorStub.Icon = _Icon
|
||||
soundGeneratorStub.Tone = _Tone
|
||||
sys.modules.setdefault("cthulhu.sound_generator", soundGeneratorStub)
|
||||
|
||||
from cthulhu import settings
|
||||
from cthulhu import sound
|
||||
from cthulhu import sound_sink
|
||||
|
||||
|
||||
class _FakeProcess:
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
|
||||
class SoundSinkTests(unittest.TestCase):
|
||||
def test_auto_sink_prefers_autoaudiosink(self):
|
||||
candidates = sound_sink._get_sink_element_candidates(settings.SOUND_SINK_AUTO)
|
||||
self.assertGreaterEqual(len(candidates), 1)
|
||||
self.assertEqual(candidates[0], "autoaudiosink")
|
||||
|
||||
|
||||
class PlayerRecoveryTests(unittest.TestCase):
|
||||
def test_worker_diagnostic_marks_restart_required(self):
|
||||
player = sound.Player()
|
||||
|
||||
player._handleWorkerDiagnostic("RECOVERY REQUIRED: lost audio sink")
|
||||
|
||||
self.assertTrue(player._workerRestartRequired)
|
||||
self.assertEqual(player._workerRestartReason, "lost audio sink")
|
||||
|
||||
def test_ensure_worker_restarts_when_recovery_is_required(self):
|
||||
player = sound.Player()
|
||||
player._workerProcess = _FakeProcess()
|
||||
player._workerSink = settings.SOUND_SINK_AUTO
|
||||
player._workerRestartRequired = True
|
||||
player._workerRestartReason = "lost audio sink"
|
||||
|
||||
stopReasons = []
|
||||
startedSinks = []
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
sound_sink,
|
||||
"get_configured_sound_sink",
|
||||
return_value=settings.SOUND_SINK_AUTO,
|
||||
),
|
||||
mock.patch.object(
|
||||
player,
|
||||
"_stopWorkerLocked",
|
||||
side_effect=lambda reason: stopReasons.append(reason),
|
||||
),
|
||||
mock.patch.object(
|
||||
player,
|
||||
"_startWorkerLocked",
|
||||
side_effect=lambda configuredSink: startedSinks.append(configuredSink) or True,
|
||||
),
|
||||
):
|
||||
self.assertTrue(player._ensureWorkerLocked())
|
||||
|
||||
self.assertEqual(stopReasons, ["lost audio sink"])
|
||||
self.assertEqual(startedSinks, [settings.SOUND_SINK_AUTO])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
112
tests/test_web_router_regressions.py
Normal file
112
tests/test_web_router_regressions.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
soundGeneratorModule = sys.modules.get("cthulhu.sound_generator")
|
||||
if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "SoundGenerator"):
|
||||
class _StubSoundGenerator:
|
||||
pass
|
||||
|
||||
soundGeneratorModule.SoundGenerator = _StubSoundGenerator
|
||||
|
||||
from cthulhu import ax_object
|
||||
from cthulhu import ax_utilities
|
||||
from cthulhu.scripts.web import script as web_script
|
||||
from cthulhu.scripts.web import script_utilities
|
||||
|
||||
|
||||
class WebClickableActivationTests(unittest.TestCase):
|
||||
def test_return_activates_click_ancestor_on_caret_context(self):
|
||||
testScript = web_script.Script.__new__(web_script.Script)
|
||||
caretObject = object()
|
||||
documentObject = object()
|
||||
|
||||
testScript.utilities = mock.Mock()
|
||||
testScript.utilities.inDocumentContent.return_value = True
|
||||
testScript.utilities.getCaretContext.return_value = (caretObject, 0)
|
||||
testScript.utilities.isClickableElement.return_value = False
|
||||
testScript.inFocusMode = mock.Mock(return_value=False)
|
||||
testScript.presentMessage = mock.Mock()
|
||||
testScript._presentDelayedMessage = mock.Mock()
|
||||
testScript._restoreFocusAfterClick = mock.Mock()
|
||||
|
||||
def has_action(obj, action_name):
|
||||
return obj is caretObject and action_name == "click-ancestor"
|
||||
|
||||
with (
|
||||
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", documentObject),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_entry", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_text", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_password_text", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_combo_box", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_button", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_push_button", return_value=False),
|
||||
mock.patch.object(ax_utilities.AXUtilities, "is_link", return_value=False),
|
||||
mock.patch.object(ax_object.AXObject, "has_action", side_effect=has_action),
|
||||
mock.patch.object(ax_object.AXObject, "do_named_action", return_value=True) as doAction,
|
||||
mock.patch("cthulhu.ax_event_synthesizer.AXEventSynthesizer.click_object") as clickObject,
|
||||
):
|
||||
self.assertTrue(testScript._tryClickableActivation(mock.Mock(event_string="Return")))
|
||||
|
||||
doAction.assert_called_once_with(caretObject, "click-ancestor")
|
||||
clickObject.assert_not_called()
|
||||
|
||||
|
||||
class WebHiddenPopupTests(unittest.TestCase):
|
||||
def test_hidden_ancestor_marks_descendant_hidden(self):
|
||||
utilities = script_utilities.Utilities.__new__(script_utilities.Utilities)
|
||||
child = object()
|
||||
hiddenParent = object()
|
||||
|
||||
utilities.inDocumentContent = mock.Mock(return_value=True)
|
||||
utilities.objectAttributes = mock.Mock(
|
||||
side_effect=lambda obj, useCache=False: {
|
||||
child: {"display": "block"},
|
||||
hiddenParent: {"display": "none"},
|
||||
}.get(obj, {})
|
||||
)
|
||||
|
||||
with mock.patch.object(
|
||||
script_utilities.AXObject,
|
||||
"get_parent",
|
||||
side_effect=lambda obj: hiddenParent if obj is child else None,
|
||||
):
|
||||
self.assertTrue(utilities.isHidden(child))
|
||||
|
||||
def test_focusable_hidden_object_cannot_have_caret_context(self):
|
||||
utilities = script_utilities.Utilities.__new__(script_utilities.Utilities)
|
||||
hiddenObject = object()
|
||||
|
||||
utilities._canHaveCaretContextDecision = {}
|
||||
utilities.isZombie = mock.Mock(return_value=False)
|
||||
utilities.isStaticTextLeaf = mock.Mock(return_value=False)
|
||||
utilities.isUselessEmptyElement = mock.Mock(return_value=False)
|
||||
utilities.isOffScreenLabel = mock.Mock(return_value=False)
|
||||
utilities.isNonNavigablePopup = mock.Mock(return_value=False)
|
||||
utilities.isUselessImage = mock.Mock(return_value=False)
|
||||
utilities.isEmptyAnchor = mock.Mock(return_value=False)
|
||||
utilities.isEmptyToolTip = mock.Mock(return_value=False)
|
||||
utilities.isParentOfNullChild = mock.Mock(return_value=False)
|
||||
utilities.isPseudoElement = mock.Mock(return_value=False)
|
||||
utilities.isFakePlaceholderForEntry = mock.Mock(return_value=False)
|
||||
utilities.isNonInteractiveDescendantOfControl = mock.Mock(return_value=False)
|
||||
utilities.isHidden = mock.Mock(return_value=True)
|
||||
utilities.hasNoSize = mock.Mock(return_value=False)
|
||||
|
||||
with (
|
||||
mock.patch.object(script_utilities.AXObject, "is_dead", return_value=False),
|
||||
mock.patch.object(script_utilities.AXUtilities, "is_focusable", return_value=True),
|
||||
):
|
||||
self.assertFalse(utilities._canHaveCaretContext(hiddenObject))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user