mirror of
https://github.com/theniceboy/.config.git
synced 2026-07-16 22:01:21 +08:00
217 lines
6.8 KiB
Python
217 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Visual test framework for agent-tracker TUI.
|
|
|
|
Captures tmux pane cells with full ANSI attribute info and runs assertions
|
|
on background colors, foreground colors, bold, and spatial layout.
|
|
"""
|
|
import sys, json, subprocess, time, os
|
|
|
|
GRID = [] # type: list
|
|
|
|
def send_keys(target, keys, delay=0.3):
|
|
subprocess.run(["tmux", "send-keys", "-t", target] + keys.split(),
|
|
capture_output=True)
|
|
time.sleep(delay)
|
|
|
|
def send_keys_literal(target, text, delay=0.2):
|
|
"""Send text literally, char by char, preserving spaces."""
|
|
for ch in text:
|
|
subprocess.run(["tmux", "send-keys", "-t", target, "-l", ch],
|
|
capture_output=True)
|
|
time.sleep(0.03)
|
|
time.sleep(delay)
|
|
|
|
def capture(target, rows=50, cols=100):
|
|
result = subprocess.run(
|
|
["python3", os.path.join(os.path.dirname(__file__), "capture_grid.py"),
|
|
target, str(rows), str(cols)],
|
|
capture_output=True, text=True
|
|
)
|
|
global GRID
|
|
new_grid = json.loads(result.stdout)
|
|
GRID.clear()
|
|
GRID.extend(new_grid)
|
|
return GRID
|
|
|
|
def cell(row, col):
|
|
if row < 0 or row >= len(GRID):
|
|
return {"ch": " ", "fg": None, "bg": None, "bold": False, "dim": False}
|
|
r = GRID[row]
|
|
if col < 0 or col >= len(r):
|
|
return {"ch": " ", "fg": None, "bg": None, "bold": False, "dim": False}
|
|
return r[col]
|
|
|
|
def row_text(row, cols=None):
|
|
if row < 0 or row >= len(GRID):
|
|
return ""
|
|
r = GRID[row]
|
|
if cols:
|
|
r = r[:cols]
|
|
return "".join(c["ch"] for c in r).rstrip()
|
|
|
|
def row_has_bg(row, bg_val, min_cols=1, max_col=None):
|
|
"""Count cells in a row that have a specific background."""
|
|
if row < 0 or row >= len(GRID):
|
|
return 0
|
|
r = GRID[row]
|
|
if max_col is None:
|
|
max_col = len(r)
|
|
count = 0
|
|
for c in r[:max_col]:
|
|
if str(c.get("bg")) == str(bg_val):
|
|
count += 1
|
|
return count
|
|
|
|
def row_bg_extent(row):
|
|
"""Return (start_col, end_col) of contiguous bg on a row, or (0,0) if none."""
|
|
if row < 0 or row >= len(GRID):
|
|
return (0, 0)
|
|
r = GRID[row]
|
|
bgs = set()
|
|
for c in r:
|
|
bg = c.get("bg")
|
|
if bg is not None and str(bg) not in ("None", "none"):
|
|
bgs.add(str(bg))
|
|
if not bgs:
|
|
return (0, 0)
|
|
# find first and last cell with any bg
|
|
first = None
|
|
last = None
|
|
for i, c in enumerate(r):
|
|
bg = c.get("bg")
|
|
if bg is not None and str(bg) not in ("None", "none"):
|
|
if first is None:
|
|
first = i
|
|
last = i
|
|
return (first if first else 0, last if last else 0)
|
|
|
|
def find_row_with_text(text, start=0, end=None):
|
|
"""Find row index containing text."""
|
|
if end is None:
|
|
end = len(GRID)
|
|
for r in range(start, end):
|
|
if text in row_text(r):
|
|
return r
|
|
return -1
|
|
|
|
PASS = 0
|
|
FAIL = 0
|
|
FAILURES = []
|
|
|
|
def check(name, condition, detail=""):
|
|
global PASS, FAIL
|
|
if condition:
|
|
PASS += 1
|
|
print(f" PASS: {name}")
|
|
else:
|
|
FAIL += 1
|
|
FAILURES.append(name)
|
|
print(f" FAIL: {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
def check_cell_bg(name, row, col, expected_bg):
|
|
c = cell(row, col)
|
|
actual = c.get("bg")
|
|
ok = str(actual) == str(expected_bg)
|
|
check(name, ok,
|
|
f"row {row} col {col}: expected bg={expected_bg}, got bg={actual} ch={repr(c['ch'])}")
|
|
|
|
def check_cell_fg(name, row, col, expected_fg):
|
|
c = cell(row, col)
|
|
actual = c.get("fg")
|
|
ok = str(actual) == str(expected_fg)
|
|
check(name, ok,
|
|
f"row {row} col {col}: expected fg={expected_fg}, got fg={actual} ch={repr(c['ch'])}")
|
|
|
|
def check_cell_bold(name, row, col, expected=True):
|
|
c = cell(row, col)
|
|
actual = c.get("bold", False)
|
|
ok = actual == expected
|
|
check(name, ok,
|
|
f"row {row} col {col}: expected bold={expected}, got bold={actual} ch={repr(c['ch'])}")
|
|
|
|
def check_row_bg_full(name, row, expected_bg, width, min_pct=0.9):
|
|
"""Check that a row has bg color extending at least min_pct of width."""
|
|
count = row_has_bg(row, expected_bg, max_col=width)
|
|
pct = count / width if width > 0 else 0
|
|
ok = pct >= min_pct
|
|
check(name, ok,
|
|
f"row {row}: expected bg={expected_bg} covering >= {min_pct*100:.0f}% of {width} cols, "
|
|
f"got {count}/{width} ({pct*100:.0f}%)")
|
|
|
|
def check_row_bg_continuous(name, row, expected_bg, expected_width):
|
|
"""Check that bg extends continuously from first cell to at least expected_width."""
|
|
if row >= len(GRID):
|
|
check(name, False, f"row {row} out of bounds")
|
|
return
|
|
r = GRID[row]
|
|
first_bg = None
|
|
for i, c in enumerate(r):
|
|
if str(c.get("bg")) == str(expected_bg):
|
|
first_bg = i
|
|
break
|
|
if first_bg is None:
|
|
check(name, False, f"row {row}: no cell with bg={expected_bg} found")
|
|
return
|
|
# check continuity from first_bg
|
|
continuous = 0
|
|
for i in range(first_bg, len(r)):
|
|
if str(r[i].get("bg")) == str(expected_bg):
|
|
continuous += 1
|
|
else:
|
|
break
|
|
ok = continuous >= expected_width
|
|
check(name, ok,
|
|
f"row {row}: expected continuous bg={expected_bg} for >= {expected_width} cols from col {first_bg}, "
|
|
f"got {continuous}")
|
|
|
|
def dump_row(row, cols=None):
|
|
"""Pretty-print a row with cell attributes."""
|
|
if row < 0 or row >= len(GRID):
|
|
print(f" [row {row}: out of bounds]")
|
|
return
|
|
r = GRID[row]
|
|
if cols:
|
|
r = r[:cols]
|
|
text = "".join(c["ch"] for c in r)
|
|
print(f" row {row}: |{text}|")
|
|
for i, c in enumerate(r):
|
|
attrs = []
|
|
if c.get("bg") and str(c["bg"]) not in ("None", "none"):
|
|
attrs.append(f"bg={c['bg']}")
|
|
if c.get("fg") and str(c["fg"]) not in ("None", "none"):
|
|
attrs.append(f"fg={c['fg']}")
|
|
if c.get("bold"):
|
|
attrs.append("bold")
|
|
if attrs and c["ch"] != " ":
|
|
print(f" col {i}: {repr(c['ch'])} [{', '.join(attrs)}]")
|
|
elif attrs and c["ch"] == " ":
|
|
# only print first few bg-only cells to avoid noise
|
|
pass
|
|
|
|
def dump_row_bg(row, cols=None):
|
|
"""Show only bg coverage for a row."""
|
|
if row < 0 or row >= len(GRID):
|
|
print(f" [row {row}: out of bounds]")
|
|
return
|
|
r = GRID[row]
|
|
if cols:
|
|
r = r[:cols]
|
|
parts = []
|
|
for i, c in enumerate(r):
|
|
bg = c.get("bg")
|
|
if bg and str(bg) not in ("None", "none"):
|
|
parts.append(str(bg))
|
|
else:
|
|
parts.append("·")
|
|
bg_str = " ".join(parts[:60])
|
|
text = "".join(c["ch"] for c in r).rstrip()
|
|
print(f" row {row} text: |{text}|")
|
|
print(f" row {row} bg: {bg_str}")
|
|
|
|
def summary():
|
|
print(f"\n{'='*40}")
|
|
print(f"Visual Results: {PASS} passed, {FAIL} failed")
|
|
print(f"{'='*40}")
|
|
if FAILURES:
|
|
print(f"Failed: {', '.join(FAILURES)}")
|
|
return FAIL
|