Improve socket handling for -x spawned fenrir instances.

This commit is contained in:
Storm Dragon
2026-05-07 23:24:54 -04:00
parent 0273f9b956
commit 8638bca1d5
53 changed files with 794 additions and 1072 deletions
+62
View File
@@ -216,6 +216,68 @@ class TestRemoteDataFormat:
assert result["success"] is False
assert "Unknown command format" in result["message"]
def test_list_instances_top_level_command(self, mock_environment):
"""Test listing registered Fenrir instances."""
self.manager.initialize(mock_environment)
with patch(
"fenrirscreenreader.core.remoteManager.remoteInstanceRegistry.list_instances",
return_value=[
{
"pid": 123,
"ppid": 100,
"screen_driver": "ptyDriver",
"keyboard_driver": "x11Driver",
"main_socket": True,
"x11_window_id": "0x123",
"socket_files": [
"/tmp/fenrirscreenreader-123.sock",
"/tmp/fenrirscreenreader-deamon.sock",
],
}
],
):
result = self.manager.handle_remote_incomming_with_response("ls")
assert result["success"] is True
assert "pid=123" in result["message"]
assert "x11_window_id=0x123" in result["message"]
assert "/tmp/fenrirscreenreader-123.sock" in result["message"]
def test_remote_incoming_suppresses_command_claimed_by_other_instance(
self, mock_environment, tmp_path
):
"""Test untargeted duplicate remote commands only run in one process."""
self.manager.initialize(mock_environment)
lock_path = tmp_path / "remote-command.lock"
lock_path.write_text("999999 1\n")
with patch.object(
self.manager,
"_get_remote_command_lock_path",
return_value=str(lock_path),
):
self.manager.handle_remote_incomming("command say duplicated")
mock_environment["runtime"]["OutputManager"].speak_text.assert_not_called()
def test_remote_incoming_allows_same_instance_repeat(
self, mock_environment, tmp_path
):
"""Test repeated direct commands to one instance are not suppressed."""
self.manager.initialize(mock_environment)
lock_path = tmp_path / "remote-command.lock"
with patch.object(
self.manager,
"_get_remote_command_lock_path",
return_value=str(lock_path),
):
self.manager.handle_remote_incomming("command say repeat")
self.manager.handle_remote_incomming("command say repeat")
assert mock_environment["runtime"]["OutputManager"].speak_text.call_count == 2
@pytest.mark.integration
@pytest.mark.remote
+22
View File
@@ -0,0 +1,22 @@
import pytest
from fenrirscreenreader.screenDriver.ptyDriver import Terminal
class DummyProcessInput:
def __init__(self):
self.data = []
def write(self, data):
self.data.append(data)
@pytest.mark.unit
def test_csi_sequences_with_intermediate_characters_do_not_render_final_byte():
terminal = Terminal(10, 3, DummyProcessInput())
terminal.feed(b"\x1b[2026$p\x1b[2048$pX")
screen = terminal.get_screen_content()
assert screen["text"].splitlines()[0] == "X "
assert "p" not in screen["text"]
+5 -2
View File
@@ -136,11 +136,14 @@ class TestDriverValidation:
"""Keyboard driver should only accept whitelisted values."""
# Valid drivers
self.manager._validate_setting_value("keyboard", "driver", "evdevDriver")
self.manager._validate_setting_value("keyboard", "driver", "ptyDriver")
self.manager._validate_setting_value("keyboard", "driver", "x11Driver")
self.manager._validate_setting_value("keyboard", "driver", "atspiDriver")
self.manager._validate_setting_value("keyboard", "driver", "dummyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "ptyDriver")
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "atspiDriver")
# Invalid driver
with pytest.raises(ValueError, match="Invalid input driver"):
self.manager._validate_setting_value("keyboard", "driver", "badDriver")
+83
View File
@@ -0,0 +1,83 @@
"""
Unit tests for automatic time announcement behavior.
"""
import datetime
import importlib.util
from pathlib import Path
from unittest.mock import Mock
def _load_time_module():
module_path = (
Path(__file__).resolve().parents[2]
/ "src"
/ "fenrirscreenreader"
/ "commands"
/ "onHeartBeat"
/ "76000-time.py"
)
spec = importlib.util.spec_from_file_location("fenrir_time_command", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _create_environment(output_manager):
now = datetime.datetime.now()
settings_manager = Mock()
settings_manager.get_setting.side_effect = lambda section, setting: {
("time", "on_minutes"): str(now.minute).zfill(2),
("general", "date_format"): "%A, %B %d, %Y",
("general", "time_format"): "%I:%M%P",
}[(section, setting)]
settings_manager.get_setting_as_int.side_effect = lambda section, setting: {
("time", "delay_sec"): 0,
}[(section, setting)]
settings_manager.get_setting_as_bool.side_effect = lambda section, setting: {
("time", "enabled"): True,
("time", "present_date"): True,
("time", "present_time"): True,
("time", "interrupt"): False,
("time", "announce"): True,
}[(section, setting)]
return {
"runtime": {
"OutputManager": output_manager,
"SettingsManager": settings_manager,
}
}
def test_time_announcement_lock_suppresses_second_instance(tmp_path):
time_module = _load_time_module()
lock_path = tmp_path / "time-announcement.lock"
first_output = Mock(
interrupt_output=Mock(),
play_sound_icon=Mock(),
present_text=Mock(),
)
second_output = Mock(
interrupt_output=Mock(),
play_sound_icon=Mock(),
present_text=Mock(),
)
first_command = time_module.command()
second_command = time_module.command()
first_command.initialize(_create_environment(first_output))
second_command.initialize(_create_environment(second_output))
first_command._get_announcement_lock_path = lambda: str(lock_path)
second_command._get_announcement_lock_path = lambda: str(lock_path)
first_command.last_time -= datetime.timedelta(hours=1)
second_command.last_time -= datetime.timedelta(hours=1)
first_command.run()
second_command.run()
first_output.play_sound_icon.assert_called_once_with("announce")
first_output.present_text.assert_called()
second_output.interrupt_output.assert_not_called()
second_output.play_sound_icon.assert_not_called()
second_output.present_text.assert_not_called()
+21 -5
View File
@@ -30,14 +30,17 @@ def test_x11_mode_runs_in_foreground():
@pytest.mark.unit
def test_x11_mode_rejects_other_emulated_modes():
def test_removed_emulated_pty_flag_is_rejected():
fenrir = load_fenrir_entrypoint()
args = fenrir.create_argument_parser().parse_args(["-x", "-e"])
with pytest.raises(SystemExit):
fenrir.create_argument_parser().parse_args(["-e"])
is_valid, error = fenrir.validate_arguments(args)
assert is_valid is False
assert "--x11" in error
@pytest.mark.unit
def test_removed_emulated_evdev_flag_is_rejected():
fenrir = load_fenrir_entrypoint()
with pytest.raises(SystemExit):
fenrir.create_argument_parser().parse_args(["-E"])
@pytest.mark.unit
@@ -50,6 +53,19 @@ def test_x11_cli_accepts_window_id():
assert args.x11_window_id == "0x123"
@pytest.mark.unit
def test_x11_window_id_requires_x11_mode():
fenrir = load_fenrir_entrypoint()
args = fenrir.create_argument_parser().parse_args(
["--x11-window-id", "0x123"]
)
is_valid, error = fenrir.validate_arguments(args)
assert is_valid is False
assert "--x11-window-id requires --x11" == error
@pytest.mark.unit
def test_x11_key_name_mapping_for_keypad_and_capslock():
x11 = X11Driver()