Add ability for X clibpard and Fenrir's clipboard to stay in sync.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.core.clipboardSyncManager import ClipboardSyncManager
|
||||
|
||||
|
||||
def build_env(
|
||||
sync_enabled=False,
|
||||
sync_display="",
|
||||
screen_driver="vcsaDriver",
|
||||
keyboard_driver="evdevDriver",
|
||||
fenrir_text=None,
|
||||
):
|
||||
def get_setting(section, setting):
|
||||
values = {
|
||||
("clipboard", "sync_display"): sync_display,
|
||||
("screen", "driver"): screen_driver,
|
||||
("keyboard", "driver"): keyboard_driver,
|
||||
}
|
||||
return values[(section, setting)]
|
||||
|
||||
settings_manager = Mock(
|
||||
get_setting_as_bool=Mock(return_value=sync_enabled),
|
||||
get_setting_as_float=Mock(return_value=0.5),
|
||||
get_setting=Mock(side_effect=get_setting),
|
||||
)
|
||||
memory_manager = Mock(
|
||||
is_index_list_empty=Mock(return_value=fenrir_text is None),
|
||||
get_index_list_element=Mock(return_value=fenrir_text),
|
||||
add_value_to_first_index=Mock(),
|
||||
)
|
||||
return {
|
||||
"runtime": {
|
||||
"SettingsManager": settings_manager,
|
||||
"MemoryManager": memory_manager,
|
||||
"DebugManager": Mock(write_debug_out=Mock()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_clipboard_sync_disabled_by_default():
|
||||
manager = ClipboardSyncManager()
|
||||
manager.start = Mock()
|
||||
|
||||
manager.initialize(build_env(sync_enabled=False))
|
||||
|
||||
assert manager.enabled is False
|
||||
manager.start.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_clipboard_sync_enabled_in_x_mode_with_current_display(monkeypatch):
|
||||
monkeypatch.setenv("DISPLAY", ":1")
|
||||
manager = ClipboardSyncManager()
|
||||
manager.start = Mock()
|
||||
|
||||
manager.initialize(
|
||||
build_env(
|
||||
sync_enabled=True,
|
||||
screen_driver="ptyDriver",
|
||||
keyboard_driver="x11Driver",
|
||||
)
|
||||
)
|
||||
|
||||
assert manager.enabled is True
|
||||
assert manager.display == ":1"
|
||||
manager.start.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_clipboard_sync_console_skips_empty_display(monkeypatch):
|
||||
monkeypatch.delenv("DISPLAY", raising=False)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.start = Mock()
|
||||
|
||||
manager.initialize(build_env(sync_enabled=True))
|
||||
|
||||
assert manager.enabled is False
|
||||
manager.start.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_clipboard_sync_console_allows_configured_display(monkeypatch):
|
||||
monkeypatch.delenv("DISPLAY", raising=False)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.start = Mock()
|
||||
|
||||
manager.initialize(build_env(sync_enabled=True, sync_display=":0"))
|
||||
|
||||
assert manager.enabled is True
|
||||
assert manager.display == ":0"
|
||||
manager.start.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_mark_written_to_x_updates_observed_state():
|
||||
manager = ClipboardSyncManager()
|
||||
|
||||
manager.mark_written_to_x("manual export")
|
||||
|
||||
assert manager.last_written_to_x == "manual export"
|
||||
assert manager.last_observed_fenrir == "manual export"
|
||||
assert manager.last_observed_x == "manual export"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_fenrir_to_x_write_is_not_reimported(monkeypatch):
|
||||
env = build_env(fenrir_text="from fenrir")
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
write_text = Mock(return_value=True)
|
||||
read_text = Mock(side_effect=[None, "from fenrir"])
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.write_text",
|
||||
write_text,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
read_text,
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
manager.poll_once()
|
||||
|
||||
write_text.assert_called_once_with("from fenrir", ":1")
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_conflicting_clipboards_do_not_swap_values(monkeypatch):
|
||||
env = build_env(fenrir_text="from fenrir")
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
write_text = Mock(return_value=True)
|
||||
read_text = Mock(return_value="from x")
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.write_text",
|
||||
write_text,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
read_text,
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
|
||||
write_text.assert_called_once_with("from fenrir", ":1")
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_not_called()
|
||||
assert manager.last_written_to_x == "from fenrir"
|
||||
assert manager.last_observed_x == "from fenrir"
|
||||
assert manager.last_observed_fenrir == "from fenrir"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_matching_clipboards_are_observed_without_duplicate_import(monkeypatch):
|
||||
env = build_env(fenrir_text="same text")
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
write_text = Mock(return_value=True)
|
||||
read_text = Mock(return_value="same text")
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.write_text",
|
||||
write_text,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
read_text,
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
|
||||
write_text.assert_not_called()
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_not_called()
|
||||
assert manager.last_observed_x == "same text"
|
||||
assert manager.last_observed_fenrir == "same text"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x_to_fenrir_import_is_not_reexported(monkeypatch):
|
||||
env = build_env(fenrir_text=None)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
write_text = Mock(return_value=True)
|
||||
read_text = Mock(return_value="from x")
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.write_text",
|
||||
write_text,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
read_text,
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
env["runtime"]["MemoryManager"].is_index_list_empty.return_value = False
|
||||
env["runtime"]["MemoryManager"].get_index_list_element.return_value = (
|
||||
"from x"
|
||||
)
|
||||
manager.poll_once()
|
||||
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_called_once_with(
|
||||
"clipboardHistory", "from x"
|
||||
)
|
||||
write_text.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_selected_imported_text_can_export_after_x_changes(monkeypatch):
|
||||
env = build_env(fenrir_text=None)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
write_text = Mock(return_value=True)
|
||||
read_text = Mock(side_effect=["from x", "other x"])
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.write_text",
|
||||
write_text,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
read_text,
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
env["runtime"]["MemoryManager"].is_index_list_empty.return_value = False
|
||||
env["runtime"]["MemoryManager"].get_index_list_element.return_value = (
|
||||
"from x"
|
||||
)
|
||||
manager.last_observed_fenrir = "different fenrir entry"
|
||||
manager.poll_once()
|
||||
|
||||
write_text.assert_called_once_with("from x", ":1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("x_value", [None, "", object()])
|
||||
def test_x_clipboard_non_text_values_are_ignored(monkeypatch, x_value):
|
||||
env = build_env(fenrir_text=None)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
Mock(return_value=x_value),
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x_clipboard_paste_exception_is_ignored(monkeypatch):
|
||||
env = build_env(fenrir_text=None)
|
||||
manager = ClipboardSyncManager()
|
||||
manager.env = env
|
||||
manager.display = ":1"
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.clipboardSyncManager.x_clipboard.read_text",
|
||||
Mock(side_effect=RuntimeError("no text target")),
|
||||
)
|
||||
|
||||
manager.poll_once()
|
||||
|
||||
env["runtime"]["MemoryManager"].add_value_to_first_index.assert_not_called()
|
||||
env["runtime"]["DebugManager"].write_debug_out.assert_called_once()
|
||||
@@ -0,0 +1,90 @@
|
||||
import os
|
||||
import subprocess
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.utils import x_clipboard
|
||||
|
||||
|
||||
def command_exists(command):
|
||||
if command == "xclip":
|
||||
return "/usr/bin/xclip"
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_write_text_uses_display_env_without_mutating_process_env(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.delenv("DISPLAY", raising=False)
|
||||
monkeypatch.setattr(x_clipboard.shutil, "which", command_exists)
|
||||
run_command = Mock(
|
||||
return_value=subprocess.CompletedProcess(
|
||||
["xclip"], 0, stdout=b"", stderr=b""
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(x_clipboard.subprocess, "run", run_command)
|
||||
|
||||
assert x_clipboard.write_text("clipboard text", ":3") is True
|
||||
|
||||
command = run_command.call_args.args[0]
|
||||
kwargs = run_command.call_args.kwargs
|
||||
assert command == ["xclip", "-selection", "clipboard"]
|
||||
assert kwargs["input"] == b"clipboard text"
|
||||
assert kwargs["env"]["DISPLAY"] == ":3"
|
||||
assert kwargs["timeout"] == 2.0
|
||||
assert os.environ.get("DISPLAY") is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_text_scans_displays_until_text_is_found(monkeypatch):
|
||||
monkeypatch.delenv("DISPLAY", raising=False)
|
||||
monkeypatch.setattr(x_clipboard.shutil, "which", command_exists)
|
||||
displays = []
|
||||
|
||||
def run_command(command, **kwargs):
|
||||
displays.append(kwargs["env"].get("DISPLAY"))
|
||||
if kwargs["env"].get("DISPLAY") == ":1":
|
||||
return subprocess.CompletedProcess(
|
||||
command, 0, stdout=b"from x", stderr=b""
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
command, 1, stdout=b"", stderr=b"missing display"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(x_clipboard.subprocess, "run", run_command)
|
||||
|
||||
assert x_clipboard.read_text(scan_displays=True) == "from x"
|
||||
assert displays == [":0", ":1"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_read_text_empty_success_returns_none(monkeypatch):
|
||||
monkeypatch.setenv("DISPLAY", ":2")
|
||||
monkeypatch.setattr(x_clipboard.shutil, "which", command_exists)
|
||||
monkeypatch.setattr(
|
||||
x_clipboard.subprocess,
|
||||
"run",
|
||||
Mock(
|
||||
return_value=subprocess.CompletedProcess(
|
||||
["xclip"], 0, stdout=b"", stderr=b""
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
assert x_clipboard.read_text() is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_write_text_propagates_clipboard_timeout(monkeypatch):
|
||||
monkeypatch.setattr(x_clipboard.shutil, "which", command_exists)
|
||||
timeout = subprocess.TimeoutExpired(["xclip"], 2.0)
|
||||
monkeypatch.setattr(
|
||||
x_clipboard.subprocess,
|
||||
"run",
|
||||
Mock(side_effect=timeout),
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.TimeoutExpired):
|
||||
x_clipboard.write_text("clipboard text", ":3")
|
||||
Reference in New Issue
Block a user