@akaoio/tester
v1.1.0
Published
Comprehensive terminal application testing framework with advanced assertions, deep state inspection, and test orchestration
Maintainers
Readme
Tester - Advanced Terminal Application Testing Framework
A powerful Go framework for testing terminal applications (TUI) in real-time, with crash detection, performance monitoring, and visual verification.
Features
🔍 Comprehensive Testing
- Virtual Terminal Emulation - Run real terminal apps in PTY
- Keyboard Simulation - Send any key combination
- Screen Capture - ASCII screenshots of terminal state
- Layout Verification - Check positioning, centering, borders
🛡️ Reliability Testing
- Crash Detection - Detect segfaults, panics, OOM kills
- Advanced Hang Detection - Multi-layer hang prevention system
- Memory Leak Detection - Monitor memory usage patterns
- Performance Monitoring - Track CPU, memory, response times
🚨 Hang Prevention System (NEW!)
- Watchdog Protection - Prevents tester from hanging when apps hang
- Operation Timeouts - Configurable timeouts for all operations
- Emergency Stop - Force termination of problematic applications
- Graceful Recovery - Automatic cleanup and restart capabilities
🎨 Visual Testing
- Color/ANSI Verification - Validate color output
- Responsive Testing - Test different terminal sizes
- Theme Testing - Verify theme changes
- Unicode Support - Test international characters
📊 Reporting
- Detailed Reports - Markdown reports with metrics
- ASCII Screenshots - Visual proof of UI states
- Performance Metrics - Response times, resource usage
- Test Logs - Complete execution traces
Installation
go get github.com/akaoio/testerQuick Start
package main
import (
"log"
"time"
"github.com/akaoio/tester"
)
func main() {
// Create virtual terminal with built-in hang protection
term := tester.NewTerminal(80, 24)
defer term.Close()
// Configure timeouts (optional - has sensible defaults)
term.SetTimeouts(30*time.Second, 5*time.Second) // global, operation
// Start your app (protected against hanging)
err := term.Start("./myapp")
if err != nil {
log.Fatal(err)
}
// All operations are now hang-protected
term.Wait(500 * time.Millisecond)
term.SendKeys("hello world") // ✅ Won't hang
term.SendKeys("<Enter>") // ✅ Won't hang
term.SendKeys("<Ctrl+C>") // ✅ Won't hang
// Screenshot with protection
screenshot := term.Screenshot()
fmt.Println(screenshot)
// Verify content
if term.Contains("expected text") {
fmt.Println("Test passed!")
}
// Check health + hang status
health := term.Health()
if health.Crashed {
log.Fatalf("App crashed: %s", health.CrashReason)
}
if term.IsHanging() {
log.Println("App is hanging - watchdog will handle it")
}
}Advanced Usage
Stress Testing
// Test crash scenarios
tester := tester.NewStressTester("./myapp")
// Try various crash scenarios
tester.TestCrash(tester.RapidInput)
tester.TestCrash(tester.InvalidEscape)
tester.TestCrash(tester.BufferOverflow)
tester.TestCrash(tester.RapidResize)
// Check for memory leaks
leaks := tester.DetectMemoryLeaks()
if len(leaks) > 0 {
log.Printf("Memory leak detected: %+v", leaks)
}
// Monitor performance
metrics := tester.GetPerformanceMetrics()
fmt.Printf("Avg Response Time: %v\n", metrics.AvgResponseTime)
fmt.Printf("Peak Memory: %d MB\n", metrics.PeakMemory/1024/1024)Visual Testing
// Test different screen sizes
sizes := []tester.Size{
{30, 10}, // Watch
{50, 20}, // Mobile
{80, 24}, // Standard
{120, 40}, // Desktop
}
for _, size := range sizes {
term := tester.NewTerminal(size.Width, size.Height)
term.Start("./myapp")
// Verify layout adapts
if !term.VerifyLayout(tester.Centered) {
log.Printf("Layout broken at %dx%d", size.Width, size.Height)
}
term.Close()
}Color Verification
term := tester.NewTerminal(80, 24)
term.Start("./myapp")
// Get color report
colors := term.GetColorReport()
if !colors.HasColors {
log.Fatal("No colors detected")
}
if colors.Has256Colors {
fmt.Println("256 color support verified")
}
fmt.Printf("ANSI sequences used: %d\n", colors.TotalSequences)Test Suite Builder
suite := tester.NewSuite("MyApp Tests")
// Add test cases
suite.AddTest(tester.TestCase{
Name: "Startup Test",
Steps: []tester.Step{
{Action: "wait", Duration: 500*time.Millisecond},
{Action: "screenshot"},
},
Assertions: []tester.Assertion{
{Type: "contains", Expected: "Welcome"},
{Type: "no_crash"},
},
})
suite.AddTest(tester.TestCase{
Name: "Keyboard Navigation",
Steps: []tester.Step{
{Action: "keys", Input: "<Tab>"},
{Action: "keys", Input: "<Enter>"},
{Action: "screenshot"},
},
Assertions: []tester.Assertion{
{Type: "contains", Expected: "Selected"},
},
})
// Run all tests
report := suite.Run("./myapp")
report.SaveMarkdown("test_report.md")
report.SaveJSON("test_report.json")Hang Prevention & Recovery
// Configure aggressive hang protection for problematic apps
term := tester.NewTerminal(80, 24)
defer term.Close()
// Set strict timeouts
term.SetTimeouts(10*time.Second, 2*time.Second) // global, operation
// Configure watchdog behavior
term.ConfigureWatchdog(tester.WatchdogConfig{
MaxResponse: 1 * time.Second, // Max response time
CheckInterval: 200 * time.Millisecond, // Check frequency
ForceKillTimeout: 1 * time.Second, // Time before force kill
})
// Start potentially problematic app
err := term.Start("./problematic-app")
if err != nil {
log.Fatal(err)
}
// All operations are protected - won't hang the tester
err = term.SendKeys("some input that might cause hang")
if err != nil {
log.Printf("Operation failed safely: %v", err)
}
// Check if hang was detected
if term.IsHanging() {
log.Println("App is hanging, but tester continues")
// Get watchdog statistics
stats := term.GetWatchdogStats()
log.Printf("Watchdog interventions: %d timeouts, %d force kills",
stats.TimeoutCount, stats.ForceKillCount)
}
// Emergency stop if needed
if term.IsHanging() {
term.ForceStop("Manual intervention")
}
// Use timeout wrapper for custom operations
err = term.WithTimeout("custom_operation", 3*time.Second, func() error {
// Your custom operation that might hang
return doSomethingRisky()
})Testing Hanging Applications
The tester framework now includes robust protection against hanging applications:
Problem Solved
- Before: Testing hanging apps would freeze the entire test suite
- After: Watchdog system detects and terminates hanging apps automatically
- Result: Test suites never hang, always complete with results
Example: Testing Dex Project
// Test the hanging dex project safely
func TestDexWithHangProtection(t *testing.T) {
term := tester.NewTerminal(120, 40)
defer term.Close()
// Configure for known problematic app
term.SetTimeouts(10*time.Second, 3*time.Second)
// Start dex (known to hang)
err := term.Start("./dex")
if err != nil {
t.Fatalf("Failed to start: %v", err)
}
// These operations won't hang the test
term.SendKeys("test input")
screenshot := term.Screenshot()
// Test completes even if dex hangs
stats := term.GetWatchdogStats()
t.Logf("Watchdog protected against %d hangs", stats.HangCount)
}Real-World Example - Testing a TUI App
func TestTUIApp(t *testing.T) {
term := tester.NewTerminal(80, 24)
defer term.Close()
// Start app
err := term.Start("./tui-app")
require.NoError(t, err)
// Test menu navigation
term.SendKeys("<Down>")
term.SendKeys("<Down>")
term.SendKeys("<Enter>")
// Verify we're in settings
assert.True(t, term.Contains("Settings"))
// Test form input
term.SendKeys("John Doe")
term.SendKeys("<Tab>")
term.SendKeys("[email protected]")
term.SendKeys("<Enter>")
// Verify form submission
assert.True(t, term.Contains("Saved successfully"))
// Check no crashes
health := term.Health()
assert.False(t, health.Crashed)
assert.False(t, health.IsHanging)
}API Reference
Terminal
type Terminal struct {
// Core methods
Start(cmd string, args ...string) error
Close() error
Wait(duration time.Duration)
// Input methods
SendKeys(keys string) error
SendRaw(bytes []byte) error
// Screen methods
Screenshot() string
GetScreen() string
Contains(text string) bool
// Verification
VerifyLayout(layout LayoutType) bool
VerifyColors() ColorReport
// Health monitoring
Health() HealthReport
IsRunning() bool
// NEW: Hang prevention and control
SetTimeouts(global, operation time.Duration)
ConfigureWatchdog(config WatchdogConfig)
GetWatchdogStats() WatchdogStats
IsHanging() bool
ForceStop(reason string)
WithTimeout(operation string, timeout time.Duration, fn func() error) error
}Key Notation
<Enter> - Enter key
<Tab> - Tab key
<Esc> - Escape
<Space> - Space bar
<Backspace> - Backspace
<Ctrl+A> - Control combinations
<Alt+X> - Alt combinations
<Shift+Tab> - Shift combinations
<Up> - Arrow keys
<Down>
<Left>
<Right>
<F1>-<F12> - Function keys
<PgUp> - Page up
<PgDown> - Page down
<Home> - Home
<End> - EndTesting Patterns
1. Smoke Test
term.Start(app)
term.Wait(1*time.Second)
assert.False(t, term.Health().Crashed)2. Navigation Test
term.SendKeys("<Tab><Tab><Enter>")
assert.True(t, term.Contains("Expected Screen"))3. Data Entry Test
term.SendKeys("[email protected]")
term.SendKeys("<Tab>")
term.SendKeys("password123")
term.SendKeys("<Enter>")4. Responsive Test
for width := 20; width <= 200; width += 20 {
term.Resize(width, 24)
assert.True(t, term.VerifyLayout(tester.Responsive))
}5. Hang Prevention Test
// Test apps that might hang
term.SetTimeouts(5*time.Second, 2*time.Second)
term.Start(problematicApp)
// Won't hang the test
err := term.SendKeys("input")
if err != nil {
t.Logf("Operation failed safely: %v", err)
}
assert.False(t, term.IsHanging())6. Emergency Recovery Test
term.Start(hangingApp)
time.Sleep(1*time.Second)
if term.IsHanging() {
term.ForceStop("Test cleanup")
time.Sleep(500*time.Millisecond)
assert.False(t, term.IsRunning())
}CI/CD Integration
GitHub Actions
name: TUI Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install dependencies
run: go mod download
- name: Run TUI tests
run: go test -v ./...
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: test_reports/Troubleshooting
"PTY not available"
- Run in a real terminal or use
scriptcommand - In Docker, use
-tflag:docker run -t
"Colors not detected"
- Ensure TERM environment variable is set
- Try
TERM=xterm-256color
"Hanging tests"
- Increase timeouts for slow systems
- Check if app requires specific environment
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details.
License
MIT License - see LICENSE file for details.
Credits
Created by AKAO.IO for testing terminal applications with confidence.
