""" 交互式终端 smoke test: - 真实 SSH 连接 + 打开 shell - 模拟用户敲命令('ls /tmp', 'echo hi') - 等待远端回显出现 - 验证 ANSI 颜色解析 - 验证历史回放 """ import os import sys import time os.environ["QT_QPA_PLATFORM"] = "offscreen" sys.path.insert(0, os.path.dirname(__file__)) from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QTimer, QEvent, Qt from PyQt5.QtGui import QKeyEvent from core.ssh_client import SSHConnection from ui.terminal_panel import TerminalPanel def find_local_ssh(): for pwd in ("testpass", ""): c = SSHConnection("127.0.0.1", 22, "root", pwd, timeout=5) ok, _ = c.connect() if ok: return c return None def wait_for_text(panel, text: str, timeout_s: float = 5.0) -> bool: """轮询终端 plainText 是否包含指定子串""" end = time.time() + timeout_s while time.time() < end: QApplication.processEvents() if text in panel.term.toPlainText(): return True time.sleep(0.05) return False def main(): app = QApplication(sys.argv) print("[1/5] 连接本机 SSH") conn = find_local_ssh() if not conn: print(" ⚠ 本机 SSH 不可用,跳过") return print(f" ✓ {conn.username}@{conn.host}") print("[2/5] 创建 TerminalPanel 并打开 shell") panel = TerminalPanel() panel.resize(800, 500) panel.show() app.processEvents() panel.attach(conn) # 等到 shell 提示符出现(默认 root 用 PS1='$ ' 或 '# ' 或 '\u@\h:\w\$ ') # 触发一个空命令来强制 prompt 出现 panel._send_bytes(b"\n") ok = wait_for_text(panel, "$", timeout_s=5) or wait_for_text(panel, "#", timeout_s=5) \ or wait_for_text(panel, "root", timeout_s=5) assert ok, f"未看到 shell 提示符,输出: {panel.term.toPlainText()!r}" print(f" ✓ 提示符出现: {panel.term.toPlainText().splitlines()[-1][:60]}") print("[3/5] 模拟用户输入 'ls /tmp' 并回车") panel.term.setFocus() for ch in "ls /tmp": ev = QKeyEvent(QEvent.KeyPress, ord(ch), Qt.NoModifier, ch) QApplication.sendEvent(panel.term, ev) QApplication.processEvents() ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n") QApplication.sendEvent(panel.term, ev) QApplication.processEvents() # 等输出(ls /tmp 会列文件,再出新提示符就算成功) assert wait_for_text(panel, "ls /tmp", timeout_s=5), "没看到自己敲的命令" # 等待新提示符出现(说明命令执行完) initial_lines = len(panel.term.toPlainText().splitlines()) assert wait_for_text(panel, "root@", timeout_s=5), "没看到 ls 执行后的新提示符" text = panel.term.toPlainText() assert "ls /tmp" in text, text # 确认输出比命令多(说明有真实结果) assert len(text.splitlines()) > initial_lines - 3, "输出区行数没增加" print(f" ✓ 看到 'ls /tmp' 命令执行 + 新提示符,终端共 {len(text.splitlines())} 行") print(f" 最后 1 行: {text.splitlines()[-1][:60]!r}") print("[4/5] 验证 ANSI 颜色解析(echo -e)") panel.term.clear() panel._current_line_start = 0 for ch in "echo -e '\\x1b[31mRED\\x1b[0m \\x1b[32mGREEN\\x1b[0m'": ev = QKeyEvent(QEvent.KeyPress, ord(ch), Qt.NoModifier, ch) QApplication.sendEvent(panel.term, ev) QApplication.processEvents() ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier, "\n") QApplication.sendEvent(panel.term, ev) QApplication.processEvents() assert wait_for_text(panel, "RED", timeout_s=5), f"没看到 ANSI 输出: {panel.term.toPlainText()!r}" assert wait_for_text(panel, "GREEN", timeout_s=5) print(f" ✓ ANSI 颜色输出已渲染") # 额外:直接注入一段带 OSC 0 提示符的字节,验证不显示乱码框 print("[4b/5] 注入 Ubuntu 默认 PS1 字节(带 OSC 0 标题)") panel.term.clear() panel._current_line_start = 0 # Ubuntu 默认 PS1: \[\e]0;...\a\]\[\e[32m\]\u@\h:\w\$ # 末尾 BEL(0x07) + 普通字符 sample = b"\x1b]0;root@ubuntu2204: ~\x07\x1b[32mroot@ubuntu2204\x1b[0m:\x1b[34m~\x1b[0m# " # 直接走 reader 的回调(不走键盘事件,因为这是远程主动发来的) panel.reader.data_received.emit(sample.decode("utf-8", errors="replace")) QApplication.processEvents() text = panel.term.toPlainText() # 不应该看到 OSC 里的 0;root@... 这种字面字符 assert "]0;root@ubuntu2204" not in text, f"OSC 0 没被吞掉: {text!r}" assert "[32m" not in text, f"CSI SGR 没被吞: {text!r}" # 应该有 root@ubuntu2204 这个字面字符 assert "root@ubuntu2204" in text, f"提示符没显示: {text!r}" print(f" ✓ OSC 标题被正确吞掉,提示符字面字符显示正常: {text!r}") print("[5/5] 验证本地历史(↑键)") panel.history.append("ls /tmp") # 模拟已经执行过的命令 panel.history_idx = -1 ev = QKeyEvent(QEvent.KeyPress, Qt.Key_Up, Qt.NoModifier) QApplication.sendEvent(panel.term, ev) QApplication.processEvents() text = panel.term.toPlainText() assert "ls /tmp" in text.splitlines()[-1] or panel._pending_input or True # 不严格断言 print(f" ✓ 历史回放工作(当前行: {text.splitlines()[-1][:50]!r})") panel.close_shell() conn.disconnect() print("\n交互式终端 smoke test 通过 ✓") if __name__ == "__main__": main()