from __future__ import annotations
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Checkpoint:
start: int
end: int
expected: list[str] = field(default_factory=list)
forbidden: list[str] = field(default_factory=list)
step: int = 10
TUI_CHECKPOINTS: dict[str, list[Checkpoint]] = {
"wt-zellij-omnibus": [
Checkpoint(
start=150,
end=350,
expected=["Opus", "acme"],
forbidden=["command not found", "Unknown command"],
),
Checkpoint(
start=1650,
end=1850,
expected=["Branch", "main"],
forbidden=["CONFLICT", "error:", "failed"],
),
],
}
def check_dependencies() -> list[str]:
missing = []
for cmd in ["ffmpeg", "tesseract"]:
result = subprocess.run(
["which", cmd], capture_output=True, text=True
)
if result.returncode != 0:
missing.append(cmd)
return missing
def extract_frames(
gif_path: Path, frames: list[int], out_dir: Path
) -> dict[int, Path]:
if not frames:
return {}
select_expr = "+".join(f"eq(n\\,{f})" for f in frames)
pattern = str(out_dir / "frame_%04d.png")
result = subprocess.run(
[
"ffmpeg",
"-loglevel", "error",
"-i", str(gif_path),
"-vf", f"select='{select_expr}'",
"-vsync", "vfr",
str(pattern),
],
capture_output=True,
)
if result.returncode != 0:
return {}
return {
frame: out_dir / f"frame_{i + 1:04d}.png"
for i, frame in enumerate(frames)
if (out_dir / f"frame_{i + 1:04d}.png").exists()
}
def ocr_image(image_path: Path) -> str:
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
output_base = f.name[:-4]
result = subprocess.run(
["tesseract", str(image_path), output_base, "-l", "eng"],
capture_output=True,
)
output_path = Path(f"{output_base}.txt")
if result.returncode == 0 and output_path.exists():
text = output_path.read_text()
output_path.unlink()
return text
return ""
def _check_patterns(
text: str,
expected: list[str],
forbidden: list[str],
) -> tuple[bool, list[str]]:
text_lower = text.lower()
errors = []
for pattern in expected:
if pattern.lower() not in text_lower:
errors.append(f"'{pattern}' not found")
for pattern in forbidden:
if pattern.lower() in text_lower:
errors.append(f"forbidden '{pattern}' present")
return len(errors) == 0, errors
def validate_checkpoint(
gif_path: Path,
checkpoint: Checkpoint,
work_dir: Path,
) -> tuple[bool, str]:
frame_numbers = list(range(checkpoint.start, checkpoint.end + 1, checkpoint.step))
frame_paths = extract_frames(gif_path, frame_numbers, work_dir)
if not frame_paths:
label = f"frames {checkpoint.start}-{checkpoint.end}"
return False, f"failed to extract {label}"
best_errors: list[str] = []
frames_checked = 0
for frame in frame_numbers:
frame_path = frame_paths.get(frame)
if frame_path is None:
continue
frames_checked += 1
text = ocr_image(frame_path)
if not text:
continue
passed, errors = _check_patterns(text, checkpoint.expected, checkpoint.forbidden)
if passed:
return True, f"matched at frame {frame} ({frames_checked} checked)"
if not best_errors or len(errors) < len(best_errors):
best_errors = errors
label = f"frames {checkpoint.start}-{checkpoint.end}"
if not frames_checked:
return False, f"no readable frames in {label}"
return False, f"no match in {label} ({frames_checked} checked): {'; '.join(best_errors)}"
def validate_tui_demo(demo_name: str, gif_path: Path) -> list[str]:
if demo_name not in TUI_CHECKPOINTS:
return [f"No checkpoints defined for demo: {demo_name}"]
if not gif_path.exists():
return [f"GIF not found: {gif_path}"]
missing = check_dependencies()
if missing:
return [f"Missing required tools: {', '.join(missing)}"]
checkpoints = TUI_CHECKPOINTS[demo_name]
all_errors = []
with tempfile.TemporaryDirectory(prefix="wt-validate-") as work_dir:
work_path = Path(work_dir)
for checkpoint in checkpoints:
passed, detail = validate_checkpoint(gif_path, checkpoint, work_path)
if not passed:
all_errors.append(detail)
return all_errors
def validate_tui_demo_verbose(demo_name: str, gif_path: Path) -> tuple[bool, str]:
lines = [f"Validating {demo_name}: {gif_path}"]
if demo_name not in TUI_CHECKPOINTS:
return False, f"No checkpoints defined for demo: {demo_name}"
if not gif_path.exists():
return False, f"GIF not found: {gif_path}"
missing = check_dependencies()
if missing:
return False, f"Missing required tools: {', '.join(missing)}"
checkpoints = TUI_CHECKPOINTS[demo_name]
all_passed = True
with tempfile.TemporaryDirectory(prefix="wt-validate-") as work_dir:
work_path = Path(work_dir)
for checkpoint in checkpoints:
passed, detail = validate_checkpoint(gif_path, checkpoint, work_path)
label = f"frames {checkpoint.start}-{checkpoint.end}"
if passed:
lines.append(f" ✓ {label}: {detail}")
else:
lines.append(f" ✗ {label}: {detail}")
all_passed = False
if all_passed:
lines.append("✓ All checkpoints passed")
else:
lines.append("✗ Some checkpoints failed")
return all_passed, "\n".join(lines)