| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | Integration test runner for FACSIMILE editor. |
| 4 | This script uses pexpect to simulate terminal interactions with the editor. |
| 5 | """ |
| 6 | |
| 7 | import sys |
| 8 | import os |
| 9 | import time |
| 10 | import tempfile |
| 11 | import subprocess |
| 12 | from typing import List, Tuple, Optional |
| 13 | |
| 14 | try: |
| 15 | import pexpect |
| 16 | except ImportError: |
| 17 | print("Error: pexpect is required. Install with: pip install pexpect") |
| 18 | sys.exit(1) |
| 19 | |
| 20 | |
| 21 | class FacsimileTest: |
| 22 | """Test harness for FACSIMILE editor.""" |
| 23 | |
| 24 | def __init__(self, binary_path: str = "./build/gfortran_*/app/fac"): |
| 25 | """Initialize test harness.""" |
| 26 | # Find the actual binary path |
| 27 | import glob |
| 28 | paths = glob.glob(binary_path) |
| 29 | if not paths: |
| 30 | raise FileNotFoundError(f"Could not find fac binary at {binary_path}") |
| 31 | self.binary_path = paths[0] |
| 32 | self.process = None |
| 33 | self.test_file = None |
| 34 | |
| 35 | def start(self, initial_content: str = "") -> None: |
| 36 | """Start the editor with a temporary file.""" |
| 37 | # Create temporary file with initial content |
| 38 | self.test_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) |
| 39 | self.test_file.write(initial_content) |
| 40 | self.test_file.flush() |
| 41 | self.test_file.close() |
| 42 | |
| 43 | # Start the editor |
| 44 | self.process = pexpect.spawn(self.binary_path, [self.test_file.name], |
| 45 | timeout=5, encoding='utf-8') |
| 46 | time.sleep(0.2) # Give editor time to initialize |
| 47 | |
| 48 | def stop(self) -> None: |
| 49 | """Stop the editor and clean up.""" |
| 50 | if self.process: |
| 51 | try: |
| 52 | # Save first to avoid unsaved changes prompt |
| 53 | self.send_key('ctrl-s') |
| 54 | time.sleep(0.1) |
| 55 | # Now quit |
| 56 | self.send_key('ctrl-q') |
| 57 | self.process.expect(pexpect.EOF, timeout=2) |
| 58 | except pexpect.TIMEOUT: |
| 59 | # Force terminate if it doesn't exit cleanly |
| 60 | self.process.terminate() |
| 61 | time.sleep(0.1) |
| 62 | except: |
| 63 | pass |
| 64 | finally: |
| 65 | self.process = None |
| 66 | |
| 67 | # Clean up temp file |
| 68 | if self.test_file: |
| 69 | try: |
| 70 | os.unlink(self.test_file.name) |
| 71 | except: |
| 72 | pass |
| 73 | self.test_file = None |
| 74 | |
| 75 | def send_key(self, key: str) -> None: |
| 76 | """Send a special key combination to the editor.""" |
| 77 | key_map = { |
| 78 | 'ctrl-a': '\x01', 'ctrl-b': '\x02', 'ctrl-c': '\x03', 'ctrl-d': '\x04', |
| 79 | 'ctrl-e': '\x05', 'ctrl-f': '\x06', 'ctrl-g': '\x07', 'ctrl-h': '\x08', |
| 80 | 'ctrl-i': '\x09', 'ctrl-j': '\x0a', 'ctrl-k': '\x0b', 'ctrl-l': '\x0c', |
| 81 | 'ctrl-m': '\x0d', 'ctrl-n': '\x0e', 'ctrl-o': '\x0f', 'ctrl-p': '\x10', |
| 82 | 'ctrl-q': '\x11', 'ctrl-r': '\x12', 'ctrl-s': '\x13', 'ctrl-t': '\x14', |
| 83 | 'ctrl-u': '\x15', 'ctrl-v': '\x16', 'ctrl-w': '\x17', 'ctrl-x': '\x18', |
| 84 | 'ctrl-y': '\x19', 'ctrl-z': '\x1a', |
| 85 | 'ctrl-shift-z': '\x1a', # Redo (may need different mapping) |
| 86 | 'escape': '\x1b', 'enter': '\n', 'tab': '\t', |
| 87 | 'backspace': '\x7f', 'delete': '\x1b[3~', |
| 88 | 'up': '\x1b[A', 'down': '\x1b[B', 'right': '\x1b[C', 'left': '\x1b[D', |
| 89 | 'home': '\x1b[H', 'end': '\x1b[F', |
| 90 | 'pageup': '\x1b[5~', 'pagedown': '\x1b[6~', |
| 91 | 'alt-[': '\x1b[', 'alt-]': '\x1b]', |
| 92 | 'alt-right': '\x1b[1;3C', 'alt-left': '\x1b[1;3D', |
| 93 | } |
| 94 | |
| 95 | if key in key_map: |
| 96 | self.process.send(key_map[key]) |
| 97 | time.sleep(0.05) # Small delay between key presses |
| 98 | else: |
| 99 | print(f"Warning: Unknown key '{key}'") |
| 100 | |
| 101 | def type_text(self, text: str) -> None: |
| 102 | """Type regular text into the editor.""" |
| 103 | for char in text: |
| 104 | self.process.send(char) |
| 105 | time.sleep(0.01) # Small delay between characters |
| 106 | |
| 107 | def save_file(self) -> None: |
| 108 | """Save the current file.""" |
| 109 | self.send_key('ctrl-s') |
| 110 | time.sleep(0.1) |
| 111 | |
| 112 | def get_file_content(self) -> str: |
| 113 | """Get the current content of the file.""" |
| 114 | self.save_file() |
| 115 | with open(self.test_file.name, 'r') as f: |
| 116 | return f.read() |
| 117 | |
| 118 | def screenshot(self) -> str: |
| 119 | """Get current screen content (for debugging).""" |
| 120 | # Send a redraw command |
| 121 | self.send_key('ctrl-l') |
| 122 | time.sleep(0.1) |
| 123 | return self.process.before or "" |
| 124 | |
| 125 | |
| 126 | class TestRunner: |
| 127 | """Run integration tests for FACSIMILE.""" |
| 128 | |
| 129 | def __init__(self): |
| 130 | self.passed = 0 |
| 131 | self.failed = 0 |
| 132 | self.tests = [] |
| 133 | |
| 134 | def add_test(self, name: str, test_func): |
| 135 | """Add a test to the suite.""" |
| 136 | self.tests.append((name, test_func)) |
| 137 | |
| 138 | def run(self) -> bool: |
| 139 | """Run all tests and return success status.""" |
| 140 | print("=" * 60) |
| 141 | print("FACSIMILE Integration Tests") |
| 142 | print("=" * 60) |
| 143 | |
| 144 | for name, test_func in self.tests: |
| 145 | editor = None |
| 146 | try: |
| 147 | editor = FacsimileTest() |
| 148 | test_func(editor) |
| 149 | print(f"✓ {name}") |
| 150 | self.passed += 1 |
| 151 | except AssertionError as e: |
| 152 | print(f"✗ {name}: {str(e)}") |
| 153 | self.failed += 1 |
| 154 | except Exception as e: |
| 155 | print(f"✗ {name}: Unexpected error: {e.__class__.__name__}: {str(e)}") |
| 156 | self.failed += 1 |
| 157 | finally: |
| 158 | if editor: |
| 159 | try: |
| 160 | editor.stop() |
| 161 | except: |
| 162 | pass # Ignore cleanup errors |
| 163 | |
| 164 | print("-" * 60) |
| 165 | print(f"Tests run: {self.passed + self.failed}, Passed: {self.passed}, Failed: {self.failed}") |
| 166 | |
| 167 | if self.failed == 0: |
| 168 | print("All tests passed! ✓") |
| 169 | return True |
| 170 | else: |
| 171 | print(f"FAILURE: {self.failed} tests failed") |
| 172 | return False |
| 173 | |
| 174 | |
| 175 | # Test functions |
| 176 | def test_basic_typing(editor: FacsimileTest): |
| 177 | """Test basic text input.""" |
| 178 | editor.start("") |
| 179 | editor.type_text("Hello World") |
| 180 | time.sleep(0.5) # Give more time for text to be typed |
| 181 | content = editor.get_file_content() |
| 182 | assert content == "Hello World", f"Expected 'Hello World', got '{content}'" |
| 183 | |
| 184 | |
| 185 | def test_navigation(editor: FacsimileTest): |
| 186 | """Test cursor navigation.""" |
| 187 | editor.start("Hello\nWorld") |
| 188 | editor.send_key('down') |
| 189 | editor.send_key('end') |
| 190 | editor.type_text("!") |
| 191 | content = editor.get_file_content() |
| 192 | assert content == "Hello\nWorld!", f"Expected 'Hello\\nWorld!', got '{content}'" |
| 193 | |
| 194 | |
| 195 | def test_auto_close_brackets(editor: FacsimileTest): |
| 196 | """Test auto-close brackets feature.""" |
| 197 | editor.start("") |
| 198 | editor.type_text("(") |
| 199 | editor.type_text("test") |
| 200 | editor.send_key('right') # Move past closing paren |
| 201 | editor.type_text(" [") |
| 202 | editor.type_text("array") |
| 203 | content = editor.get_file_content() |
| 204 | assert "(test) [array]" in content, f"Auto-close failed: '{content}'" |
| 205 | |
| 206 | |
| 207 | def test_search(editor: FacsimileTest): |
| 208 | """Test search functionality.""" |
| 209 | editor.start("Hello World\nHello Universe\nHello Galaxy") |
| 210 | editor.type_text("/") |
| 211 | time.sleep(0.1) |
| 212 | editor.type_text("Universe") |
| 213 | editor.send_key('enter') |
| 214 | # Cursor should now be on "Universe" |
| 215 | editor.send_key('ctrl-k') # Kill line forward |
| 216 | content = editor.get_file_content() |
| 217 | assert "Hello Universe" not in content or "Universe" not in content.split('\n')[1] |
| 218 | |
| 219 | |
| 220 | def test_undo_redo(editor: FacsimileTest): |
| 221 | """Test undo/redo functionality.""" |
| 222 | editor.start("") |
| 223 | editor.type_text("First") |
| 224 | editor.send_key('ctrl-z') # Undo |
| 225 | content = editor.get_file_content() |
| 226 | assert content == "", f"Undo failed: '{content}'" |
| 227 | |
| 228 | editor.send_key('ctrl-shift-z') # Redo |
| 229 | content = editor.get_file_content() |
| 230 | assert content == "First", f"Redo failed: '{content}'" |
| 231 | |
| 232 | |
| 233 | def test_indent_dedent(editor: FacsimileTest): |
| 234 | """Test tab/shift-tab indent/dedent.""" |
| 235 | editor.start("Line 1\nLine 2\nLine 3") |
| 236 | # Select all lines (simplified - would need proper selection in real test) |
| 237 | editor.send_key('ctrl-a') # Go to start |
| 238 | editor.type_text(" ") # Manual indent for this test |
| 239 | content = editor.get_file_content() |
| 240 | assert content.startswith(" "), f"Indent failed: '{content}'" |
| 241 | |
| 242 | |
| 243 | def test_bracket_jump(editor: FacsimileTest): |
| 244 | """Test jump to matching bracket.""" |
| 245 | editor.start("(hello [world] test)") |
| 246 | editor.send_key('alt-]') # Jump to matching bracket |
| 247 | editor.type_text("X") |
| 248 | content = editor.get_file_content() |
| 249 | # This would need more sophisticated testing to verify cursor position |
| 250 | |
| 251 | |
| 252 | def test_smart_home(editor: FacsimileTest): |
| 253 | """Test smart home behavior.""" |
| 254 | editor.start(" Hello World") |
| 255 | editor.send_key('end') |
| 256 | editor.send_key('ctrl-a') # Should go to 'H' |
| 257 | editor.type_text("X") |
| 258 | content = editor.get_file_content() |
| 259 | assert "XHello" in content or " XHello" in content |
| 260 | |
| 261 | |
| 262 | def main(): |
| 263 | """Run all integration tests.""" |
| 264 | runner = TestRunner() |
| 265 | |
| 266 | # Add all test functions |
| 267 | runner.add_test("Basic typing", test_basic_typing) |
| 268 | runner.add_test("Navigation", test_navigation) |
| 269 | runner.add_test("Auto-close brackets", test_auto_close_brackets) |
| 270 | runner.add_test("Search", test_search) |
| 271 | runner.add_test("Undo/Redo", test_undo_redo) |
| 272 | runner.add_test("Indent/Dedent", test_indent_dedent) |
| 273 | runner.add_test("Bracket jump", test_bracket_jump) |
| 274 | runner.add_test("Smart home", test_smart_home) |
| 275 | |
| 276 | # Run tests |
| 277 | success = runner.run() |
| 278 | sys.exit(0 if success else 1) |
| 279 | |
| 280 | |
| 281 | if __name__ == "__main__": |
| 282 | main() |