Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

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)