91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
313 lines
9.3 KiB
Python
313 lines
9.3 KiB
Python
"""Unit tests for cua_auto.terminal — cross-platform PTY engine.
|
|
|
|
Covers Unix PTY paths (echo, spaces, stdin interaction, exit codes, kill, resize).
|
|
Windows-only paths are skipped when not running on win32 (and vice-versa).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
# All tests in this module require a real PTY, so skip on platforms where we
|
|
# can't spawn one without the optional dependency.
|
|
pytestmark = pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Unix PTY tests — win32 uses pywinpty (tested separately)",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _run_command(cmd: str, timeout: float = 3.0) -> tuple[bytes, int]:
|
|
"""Spawn *cmd* in a PTY, collect all output, return (output, exit_code)."""
|
|
from cua_auto.terminal import Terminal
|
|
|
|
chunks: list[bytes] = []
|
|
t = Terminal()
|
|
session = t.create(command=cmd, cols=80, rows=24, on_data=chunks.append)
|
|
exit_code = t.wait(session.pid, timeout=timeout)
|
|
return b"".join(chunks), exit_code or 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Import / singleton
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTerminalImport:
|
|
def test_module_importable(self):
|
|
import cua_auto.terminal # noqa: F401
|
|
|
|
def test_singleton_exists(self):
|
|
from cua_auto.terminal import Terminal, terminal
|
|
|
|
assert isinstance(terminal, Terminal)
|
|
|
|
def test_pty_session_dataclass(self):
|
|
from cua_auto.terminal import PtySession
|
|
|
|
s = PtySession(pid=1234, cols=80, rows=24)
|
|
assert s.pid == 1234
|
|
assert s.cols == 80
|
|
assert s.rows == 24
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Basic echo
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEchoBasic:
|
|
def test_echo_hello(self):
|
|
output, code = _run_command("echo hello")
|
|
assert b"hello" in output
|
|
assert code == 0
|
|
|
|
def test_echo_with_spaces(self):
|
|
output, code = _run_command("echo hello world")
|
|
assert b"hello world" in output
|
|
assert code == 0
|
|
|
|
def test_echo_leading_trailing_spaces(self):
|
|
output, code = _run_command("echo ' spaced '")
|
|
assert b"spaced" in output
|
|
assert code == 0
|
|
|
|
def test_echo_multiple_words(self):
|
|
output, code = _run_command("echo foo bar baz")
|
|
assert b"foo" in output
|
|
assert b"bar" in output
|
|
assert b"baz" in output
|
|
assert code == 0
|
|
|
|
def test_echo_empty_string(self):
|
|
# echo with an empty-ish arg should still exit 0
|
|
output, code = _run_command("echo ''")
|
|
assert code == 0
|
|
|
|
def test_echo_numbers(self):
|
|
output, code = _run_command("echo 42")
|
|
assert b"42" in output
|
|
assert code == 0
|
|
|
|
def test_echo_special_chars(self):
|
|
output, code = _run_command("echo 'hello-world_test'")
|
|
assert b"hello-world_test" in output
|
|
assert code == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exit codes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExitCodes:
|
|
def test_exit_zero(self):
|
|
_, code = _run_command("exit 0")
|
|
assert code == 0
|
|
|
|
def test_exit_nonzero(self):
|
|
_, code = _run_command("exit 1")
|
|
assert code == 1
|
|
|
|
def test_exit_42(self):
|
|
_, code = _run_command("exit 42")
|
|
assert code == 42
|
|
|
|
def test_true_command(self):
|
|
_, code = _run_command("true")
|
|
assert code == 0
|
|
|
|
def test_false_command(self):
|
|
_, code = _run_command("false")
|
|
assert code != 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Interactive stdin (send_stdin)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSendStdin:
|
|
def test_send_echo_via_stdin(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
chunks: list[bytes] = []
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=chunks.append)
|
|
|
|
# Give bash a moment to start
|
|
time.sleep(0.2)
|
|
t.send_stdin(session.pid, b"echo hello_from_stdin\n")
|
|
t.send_stdin(session.pid, b"exit\n")
|
|
t.wait(session.pid, timeout=3.0)
|
|
|
|
output = b"".join(chunks)
|
|
assert b"hello_from_stdin" in output
|
|
|
|
def test_send_echo_with_spaces_via_stdin(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
chunks: list[bytes] = []
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=chunks.append)
|
|
|
|
time.sleep(0.2)
|
|
t.send_stdin(session.pid, b"echo 'hello world from stdin'\n")
|
|
t.send_stdin(session.pid, b"exit\n")
|
|
t.wait(session.pid, timeout=3.0)
|
|
|
|
output = b"".join(chunks)
|
|
assert b"hello world from stdin" in output
|
|
|
|
def test_send_multiple_commands(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
chunks: list[bytes] = []
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=chunks.append)
|
|
|
|
time.sleep(0.2)
|
|
t.send_stdin(session.pid, b"echo first\n")
|
|
t.send_stdin(session.pid, b"echo second\n")
|
|
t.send_stdin(session.pid, b"exit\n")
|
|
t.wait(session.pid, timeout=3.0)
|
|
|
|
output = b"".join(chunks)
|
|
assert b"first" in output
|
|
assert b"second" in output
|
|
|
|
def test_exit_code_via_stdin(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=lambda _: None)
|
|
|
|
time.sleep(0.2)
|
|
t.send_stdin(session.pid, b"exit 7\n")
|
|
code = t.wait(session.pid, timeout=3.0)
|
|
assert code == 7
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Kill
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKill:
|
|
def test_kill_running_session(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
# sleep for a long time — we'll kill it
|
|
session = t.create(command="sleep 60", cols=80, rows=24, on_data=lambda _: None)
|
|
|
|
killed = t.kill(session.pid)
|
|
assert killed is True
|
|
|
|
# After kill, wait should return quickly
|
|
code = t.wait(session.pid, timeout=2.0)
|
|
assert code is not None # process has exited
|
|
|
|
def test_kill_unknown_pid_returns_false(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
assert t.kill(9999999) is False
|
|
|
|
def test_wait_unknown_pid_returns_none(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
assert t.wait(9999999, timeout=0.1) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resize
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestResize:
|
|
def test_resize_does_not_raise(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=lambda _: None)
|
|
|
|
# Resize should not raise
|
|
t.resize(session.pid, 120, 40)
|
|
t.resize(session.pid, 80, 24)
|
|
|
|
t.send_stdin(session.pid, b"exit\n")
|
|
t.wait(session.pid, timeout=2.0)
|
|
|
|
def test_resize_unknown_pid_is_noop(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
# Should not raise even for unknown pids
|
|
t.resize(9999999, 80, 24)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Connect (callback replacement)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConnect:
|
|
def test_connect_replaces_callback(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
first_chunks: list[bytes] = []
|
|
second_chunks: list[bytes] = []
|
|
|
|
t = Terminal()
|
|
session = t.create(command="bash", cols=80, rows=24, on_data=first_chunks.append)
|
|
|
|
time.sleep(0.2)
|
|
t.send_stdin(session.pid, b"echo before_connect\n")
|
|
time.sleep(0.1)
|
|
|
|
# Re-attach with a new callback
|
|
t.connect(session.pid, second_chunks.append)
|
|
|
|
t.send_stdin(session.pid, b"echo after_connect\n")
|
|
t.send_stdin(session.pid, b"exit\n")
|
|
t.wait(session.pid, timeout=3.0)
|
|
|
|
# "after_connect" should appear in the second callback's data
|
|
assert b"after_connect" in b"".join(second_chunks)
|
|
|
|
def test_connect_unknown_pid_raises(self):
|
|
from cua_auto.terminal import Terminal
|
|
|
|
t = Terminal()
|
|
with pytest.raises(KeyError):
|
|
t.connect(9999999, lambda _: None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Singleton convenience
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSingletonTerminal:
|
|
def test_singleton_echo(self):
|
|
from cua_auto.terminal import terminal
|
|
|
|
chunks: list[bytes] = []
|
|
session = terminal.create(
|
|
command="echo singleton_works",
|
|
cols=80,
|
|
rows=24,
|
|
on_data=chunks.append,
|
|
)
|
|
terminal.wait(session.pid, timeout=3.0)
|
|
assert b"singleton_works" in b"".join(chunks)
|