mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-10-30 16:10:36 +00:00
nixos/test-driver: implement nspawn keyboard input
Keyboard input helpers currently live on QemuMachine and send keys through the QEMU monitor. Tests moved to nspawn must consequently know about the backend and invoke xdotool themselves. uinput would require exposing the host's /dev/uinput to the sandbox. Its device belongs to the shared kernel input subsystem rather than to a container-private kernel, adding a privileged host dependency. https://docs.kernel.org/input/uinput.html Define send_key on BaseMachine and move the backend-neutral send_chars helper there. Keep QEMU input unchanged, and translate the portable key vocabulary to X11 key names for nspawn. Install xdotool in instrumented nspawn containers. This lets tests continue to express keyboard input through the machine interface regardless of backend. Assisted-by: Codex gpt-5.6-sol high
This commit is contained in:
@@ -101,6 +101,56 @@ CHAR_TO_KEY = {
|
||||
")": "shift-0x0B",
|
||||
}
|
||||
|
||||
# send_key uses QEMU key names, with "\n" as a driver alias for Return.
|
||||
# QEMU names: https://www.qemu.org/docs/master/interop/qemu-qmp-ref.html#enum-QKeyCode
|
||||
# xdotool names: https://github.com/jordansissel/xdotool/blob/main/xdotool.pod#keyboard-commands
|
||||
# QEMU key name (or "\n") -> X11 keysym name.
|
||||
XDOTOOL_KEY_ALIASES = {
|
||||
"\n": "Return",
|
||||
"alt": "Alt_L",
|
||||
"alt_r": "Alt_R",
|
||||
"backspace": "BackSpace",
|
||||
"delete": "Delete",
|
||||
"down": "Down",
|
||||
"esc": "Escape",
|
||||
"kp_enter": "KP_Enter",
|
||||
"left": "Left",
|
||||
"meta_l": "Super_L",
|
||||
"meta_r": "Super_R",
|
||||
"ret": "Return",
|
||||
"right": "Right",
|
||||
"shift": "Shift_L",
|
||||
"spc": "space",
|
||||
"tab": "Tab",
|
||||
"up": "Up",
|
||||
}
|
||||
|
||||
# QEMU modifier name -> xdotool modifier name.
|
||||
XDOTOOL_MODIFIER_ALIASES = {
|
||||
"alt": "alt",
|
||||
"ctrl": "ctrl",
|
||||
"meta_l": "super",
|
||||
"meta_r": "super",
|
||||
"shift": "shift",
|
||||
}
|
||||
|
||||
|
||||
def xdotool_key_sequence(key: str) -> str:
|
||||
"""Translate a QEMU-style key chord to xdotool syntax.
|
||||
|
||||
For example, "ctrl-alt-ret" becomes "ctrl+alt+Return".
|
||||
"""
|
||||
parts = key.split("-")
|
||||
modifiers = []
|
||||
while len(parts) > 1 and parts[0] in XDOTOOL_MODIFIER_ALIASES:
|
||||
modifiers.append(XDOTOOL_MODIFIER_ALIASES[parts.pop(0)])
|
||||
|
||||
base = "-".join(parts)
|
||||
base = XDOTOOL_KEY_ALIASES.get(base, base)
|
||||
if re.fullmatch(r"f\d+", base):
|
||||
base = base.upper()
|
||||
return "+".join([*modifiers, base])
|
||||
|
||||
|
||||
def make_command(args: list) -> str:
|
||||
return " ".join(map(shlex.quote, (map(str, args))))
|
||||
@@ -624,6 +674,44 @@ class BaseMachine(ABC):
|
||||
with self.nested("waiting for a window to appear"):
|
||||
retry(window_is_visible, as_timedelta(timeout))
|
||||
|
||||
@abstractmethod
|
||||
def send_key(
|
||||
self,
|
||||
key: str,
|
||||
delay: Duration | None = dt.timedelta(milliseconds=10),
|
||||
log: bool | None = True,
|
||||
) -> None:
|
||||
"""
|
||||
Simulate pressing a key or key chord, e.g.,
|
||||
`send_key("ctrl-alt-delete")`.
|
||||
|
||||
Key names follow QEMU's sendkey vocabulary: `ret` means Enter and
|
||||
`spc` means Space. Named keys and modifiers are case-sensitive;
|
||||
use lowercase names, e.g. `f1` or `ctrl-alt-f1`. Literal characters
|
||||
preserve case: `F` types uppercase F, while `f` types lowercase f.
|
||||
See https://www.qemu.org/docs/master/interop/qemu-qmp-ref.html#enum-QKeyCode.
|
||||
|
||||
Portable key names include printable ASCII characters, function keys,
|
||||
`tab`, `ret`, `esc`, `spc`, `backspace`, `delete`, `left`, `right`,
|
||||
`up`, `down`, and `kp_enter`. Chords may use the `ctrl`, `alt`,
|
||||
`shift`, `meta_l`, and `meta_r` modifiers. Machine backends may accept
|
||||
additional key names.
|
||||
"""
|
||||
...
|
||||
|
||||
def send_chars(
|
||||
self, chars: str, delay: Duration | None = dt.timedelta(milliseconds=10)
|
||||
) -> None:
|
||||
r"""
|
||||
Simulate typing a sequence of characters on the virtual keyboard,
|
||||
e.g., `send_chars("foobar\n")` will type the string `foobar`
|
||||
followed by the Enter key.
|
||||
"""
|
||||
_warn_if_numeric_duration(delay, "send_chars")
|
||||
with self.nested(f"sending keys {repr(chars)}"):
|
||||
for char in chars:
|
||||
self.send_key(char, delay, log=False)
|
||||
|
||||
@contextmanager
|
||||
def _managed_screenshot(self) -> Generator[Path]:
|
||||
"""
|
||||
@@ -1226,19 +1314,6 @@ class QemuMachine(BaseMachine):
|
||||
if elapsed >= timeout:
|
||||
raise TimeoutError
|
||||
|
||||
def send_chars(
|
||||
self, chars: str, delay: Duration | None = dt.timedelta(milliseconds=10)
|
||||
) -> None:
|
||||
r"""
|
||||
Simulate typing a sequence of characters on the virtual keyboard,
|
||||
e.g., `send_chars("foobar\n")` will type the string `foobar`
|
||||
followed by the Enter key.
|
||||
"""
|
||||
_warn_if_numeric_duration(delay, "send_chars")
|
||||
with self.nested(f"sending keys {repr(chars)}"):
|
||||
for char in chars:
|
||||
self.send_key(char, delay, log=False)
|
||||
|
||||
def wait_for_file(
|
||||
self, filename: str, timeout: Duration = dt.timedelta(minutes=15)
|
||||
) -> None:
|
||||
@@ -1363,13 +1438,6 @@ class QemuMachine(BaseMachine):
|
||||
delay: Duration | None = dt.timedelta(milliseconds=10),
|
||||
log: bool | None = True,
|
||||
) -> None:
|
||||
"""
|
||||
Simulate pressing keys on the virtual keyboard, e.g.,
|
||||
`send_key("ctrl-alt-delete")`.
|
||||
|
||||
Please also refer to the QEMU documentation for more information on the
|
||||
input syntax: https://en.wikibooks.org/wiki/QEMU/Monitor#sendkey_keys
|
||||
"""
|
||||
_warn_if_numeric_duration(delay, "send_key")
|
||||
key = CHAR_TO_KEY.get(key, key)
|
||||
context = self.nested(f"sending key {repr(key)}") if log else nullcontext()
|
||||
@@ -1671,6 +1739,37 @@ class NspawnMachine(BaseMachine):
|
||||
with self.nested("waiting for the X11 server"):
|
||||
retry(check_x, as_timedelta(timeout))
|
||||
|
||||
def send_key(
|
||||
self,
|
||||
key: str,
|
||||
delay: Duration | None = dt.timedelta(milliseconds=10),
|
||||
log: bool | None = True,
|
||||
) -> None:
|
||||
_warn_if_numeric_duration(delay, "send_key")
|
||||
context = self.nested(f"sending key {repr(key)}") if log else nullcontext()
|
||||
with context:
|
||||
# Type literal characters to avoid key syntax, e.g. "+" as a chord separator.
|
||||
if len(key) == 1 and key.isprintable():
|
||||
command = [
|
||||
"xdotool",
|
||||
"type",
|
||||
"--clearmodifiers",
|
||||
"--delay",
|
||||
0,
|
||||
"--",
|
||||
key,
|
||||
]
|
||||
else:
|
||||
command = [
|
||||
"xdotool",
|
||||
"key",
|
||||
"--clearmodifiers",
|
||||
xdotool_key_sequence(key),
|
||||
]
|
||||
self.succeed(make_command(command))
|
||||
if delay is not None:
|
||||
time.sleep(as_seconds(delay))
|
||||
|
||||
@contextmanager
|
||||
def _managed_screenshot(self) -> Generator[Path]:
|
||||
# xwd writes inside the container and xwdtopnm reads on the host, so
|
||||
|
||||
@@ -233,11 +233,13 @@ in
|
||||
environment.systemPackages = [
|
||||
pkgs.xwininfo
|
||||
]
|
||||
++ lib.optional config.boot.isNspawnContainer (
|
||||
++ lib.optionals config.boot.isNspawnContainer [
|
||||
# Unlike a QEMU machine, an nspawn container has no monitor that can
|
||||
# capture its display, so the test driver uses `xwd' inside the container.
|
||||
# capture its display or inject keyboard input, so the test driver uses
|
||||
# `xwd' and `xdotool' inside the container instead.
|
||||
pkgs.xdotool
|
||||
pkgs.xwd
|
||||
);
|
||||
];
|
||||
|
||||
# Log everything to the serial console.
|
||||
services.journald.settings.Journal = {
|
||||
|
||||
Reference in New Issue
Block a user