fortsh Interactive Test Framework
Automated testing framework for fortsh's interactive features using Python and pexpect.
Quick Start
# Activate the virtual environment
source tests/interactive/.venv/bin/activate
# Run all YAML spec tests
python tests/interactive/run_tests.py
# Run with specific fortsh binary
python tests/interactive/run_tests.py --fortsh ./bin/fortsh
# Run pytest tests
python tests/interactive/run_tests.py --pytest
# Generate markdown report
python tests/interactive/run_tests.py --report manual/results/$(date +%Y%m%d).md
Directory Structure
tests/interactive/
├── run_tests.py # Main test runner
├── fortsh_pty.py # PTY management class
├── conftest.py # Pytest fixtures
├── requirements.txt # Python dependencies
├── test_specs/ # YAML test specifications
│ ├── line_editing.yaml # Line editing tests (49 tests)
│ ├── history.yaml # History navigation/expansion (37 tests)
│ ├── completion.yaml # Tab completion tests (37 tests)
│ ├── signals_jobs.yaml # Signals and job control (40 tests)
│ ├── prompt_display.yaml # Prompt and display tests (39 tests)
│ └── posix.yaml # POSIX shell features (119 tests)
├── utils/
│ ├── keys.py # Key sequence definitions
│ └── matchers.py # Output matching utilities
├── manual/
│ └── results/ # Test result reports
└── README.md # This file
Writing Tests
YAML Specification Format
Tests can be defined declaratively in YAML:
metadata:
category: "Line Editing"
description: "Tests for cursor movement"
tests:
- name: "Left arrow moves cursor back"
steps:
- send: "echo test"
- send_key: "Left"
- send_key: "Left"
- send: "X"
- send_key: "Enter"
expect_output: "teXst"
match_type: "contains"
Available Step Types
| Step | Description | Example |
|---|---|---|
send |
Send text without newline | send: "echo hello" |
send_line |
Send text with Enter | send_line: "echo hello" |
send_key |
Send special key | send_key: "C-a" |
send_keys |
Send multiple keys | send_keys: ["Left", "Left"] |
wait |
Sleep for seconds | wait: 0.5 |
wait_for_prompt |
Wait for shell prompt | wait_for_prompt: true |
expect |
Wait for pattern | expect: "hello" |
resize |
Change terminal size | resize: {rows: 40, cols: 120} |
Match Types
| Type | Description |
|---|---|
exact |
Exact match (after strip) |
contains |
Substring match |
regex |
Regular expression |
startswith |
Prefix match |
endswith |
Suffix match |
Pytest Tests
For complex tests, use Python with pytest fixtures:
import pytest
@pytest.mark.line_editing
def test_ctrl_a_moves_to_beginning(fortsh):
fortsh.send("hello world")
fortsh.send_key("C-a")
fortsh.send("echo ")
fortsh.send_key("Enter")
output = fortsh.wait_for_prompt()
assert "hello world" in output
Available Fixtures
fortsh- Running fortsh session (rc disabled)fortsh_with_rc- Session with user's .fortshrcfortsh_factory- Create multiple sessionsfortsh_path- Path to fortsh binary
Key Sequences
Common keys are defined in utils/keys.py:
# Control keys
"C-a" # Beginning of line
"C-e" # End of line
"C-k" # Kill to end
"C-u" # Kill to beginning
"C-w" # Kill word
"C-y" # Yank
"C-c" # Interrupt
"C-z" # Suspend
"C-d" # EOF/Delete
"C-r" # Reverse search
# Alt/Meta keys
"M-b" # Back word
"M-f" # Forward word
# Arrow keys
"Up", "Down", "Left", "Right"
# Special
"Tab", "Enter", "Backspace", "Delete"
"Home", "End", "PageUp", "PageDown"
Test Categories
Tests are organized by feature (total 321 tests):
-
POSIX Shell Features (120+ tests)
- Basic operations
- Quoting and escaping
- Variables and expansion
- Pipelines and redirections
- Control structures (if/for/while/case)
- Functions and arithmetic
- Builtins
-
Line Editing (49 tests)
- Cursor movement
- Text modification
- Kill ring operations
- Word operations
-
History (37 tests)
- Arrow key navigation
- Ctrl+R search
- History expansion (!!, !$, etc.)
-
Completion (37 tests)
- Command completion
- Path/file completion
- Variable completion
-
Signals & Job Control (40 tests)
- SIGINT (Ctrl+C)
- SIGTSTP (Ctrl+Z)
- Background jobs
- Job specs (%n, %%, %+)
- fg/bg/jobs builtins
-
Prompt & Display (39 tests)
- PS1/PS2 escapes
- Terminal resize
- Colors and Unicode
Running Specific Tests
# Run single spec file
python tests/interactive/run_tests.py --spec line_editing.yaml
# Run pytest with markers
pytest tests/interactive -m line_editing
pytest tests/interactive -m "not slow"
# Run with verbose output
python tests/interactive/run_tests.py -v
Environment Variables
FORTSH- Path to fortsh binaryFORTSH_RC_FILE- Override rc file path
Adding New Tests
1. Add to existing YAML spec
# In test_specs/line_editing.yaml
tests:
- name: "My new test"
steps:
- send_line: "echo test"
expect_output: "test"
2. Create new YAML spec
# Create tests/interactive/test_specs/my_feature.yaml
3. Add pytest test
# Create tests/interactive/test_my_feature.py
def test_my_feature(fortsh):
output = fortsh.run_command("echo hello")
assert "hello" in output
CI Integration
For GitHub Actions with tmux:
- name: Run interactive tests
run: |
tmux new-session -d -s test
tmux send-keys -t test "python tests/interactive/run_tests.py" Enter
sleep 60
tmux capture-pane -t test -p > test_output.txt
grep -q "ALL TESTS PASSED" test_output.txt
Troubleshooting
"fortsh binary not found"
# Build fortsh first
make clean && make
# Or specify path
python tests/interactive/run_tests.py --fortsh /path/to/fortsh
Test timeouts
Increase timeout in test or PTY initialization:
pty = FortshPTY(timeout=10.0) # 10 seconds
Debugging tests
# In pytest test
def test_debug(fortsh):
fortsh.send_line("echo hello")
import time; time.sleep(5) # Pause to observe
output = fortsh.wait_for_prompt()
print(f"Output: {output}") # Will show with -s flag
Run with:
pytest tests/interactive -s --tb=long
Dependencies
- Python 3.8+
- pexpect >= 4.8
- PyYAML >= 6.0
- pytest >= 7.0
- colorama >= 0.4
Install with:
pip install -r tests/interactive/requirements.txt
View source
| 1 | # fortsh Interactive Test Framework |
| 2 | |
| 3 | Automated testing framework for fortsh's interactive features using Python and pexpect. |
| 4 | |
| 5 | ## Quick Start |
| 6 | |
| 7 | ```bash |
| 8 | # Activate the virtual environment |
| 9 | source tests/interactive/.venv/bin/activate |
| 10 | |
| 11 | # Run all YAML spec tests |
| 12 | python tests/interactive/run_tests.py |
| 13 | |
| 14 | # Run with specific fortsh binary |
| 15 | python tests/interactive/run_tests.py --fortsh ./bin/fortsh |
| 16 | |
| 17 | # Run pytest tests |
| 18 | python tests/interactive/run_tests.py --pytest |
| 19 | |
| 20 | # Generate markdown report |
| 21 | python tests/interactive/run_tests.py --report manual/results/$(date +%Y%m%d).md |
| 22 | ``` |
| 23 | |
| 24 | ## Directory Structure |
| 25 | |
| 26 | ``` |
| 27 | tests/interactive/ |
| 28 | ├── run_tests.py # Main test runner |
| 29 | ├── fortsh_pty.py # PTY management class |
| 30 | ├── conftest.py # Pytest fixtures |
| 31 | ├── requirements.txt # Python dependencies |
| 32 | ├── test_specs/ # YAML test specifications |
| 33 | │ ├── line_editing.yaml # Line editing tests (49 tests) |
| 34 | │ ├── history.yaml # History navigation/expansion (37 tests) |
| 35 | │ ├── completion.yaml # Tab completion tests (37 tests) |
| 36 | │ ├── signals_jobs.yaml # Signals and job control (40 tests) |
| 37 | │ ├── prompt_display.yaml # Prompt and display tests (39 tests) |
| 38 | │ └── posix.yaml # POSIX shell features (119 tests) |
| 39 | ├── utils/ |
| 40 | │ ├── keys.py # Key sequence definitions |
| 41 | │ └── matchers.py # Output matching utilities |
| 42 | ├── manual/ |
| 43 | │ └── results/ # Test result reports |
| 44 | └── README.md # This file |
| 45 | ``` |
| 46 | |
| 47 | ## Writing Tests |
| 48 | |
| 49 | ### YAML Specification Format |
| 50 | |
| 51 | Tests can be defined declaratively in YAML: |
| 52 | |
| 53 | ```yaml |
| 54 | metadata: |
| 55 | category: "Line Editing" |
| 56 | description: "Tests for cursor movement" |
| 57 | |
| 58 | tests: |
| 59 | - name: "Left arrow moves cursor back" |
| 60 | steps: |
| 61 | - send: "echo test" |
| 62 | - send_key: "Left" |
| 63 | - send_key: "Left" |
| 64 | - send: "X" |
| 65 | - send_key: "Enter" |
| 66 | expect_output: "teXst" |
| 67 | match_type: "contains" |
| 68 | ``` |
| 69 | |
| 70 | #### Available Step Types |
| 71 | |
| 72 | | Step | Description | Example | |
| 73 | |------|-------------|---------| |
| 74 | | `send` | Send text without newline | `send: "echo hello"` | |
| 75 | | `send_line` | Send text with Enter | `send_line: "echo hello"` | |
| 76 | | `send_key` | Send special key | `send_key: "C-a"` | |
| 77 | | `send_keys` | Send multiple keys | `send_keys: ["Left", "Left"]` | |
| 78 | | `wait` | Sleep for seconds | `wait: 0.5` | |
| 79 | | `wait_for_prompt` | Wait for shell prompt | `wait_for_prompt: true` | |
| 80 | | `expect` | Wait for pattern | `expect: "hello"` | |
| 81 | | `resize` | Change terminal size | `resize: {rows: 40, cols: 120}` | |
| 82 | |
| 83 | #### Match Types |
| 84 | |
| 85 | | Type | Description | |
| 86 | |------|-------------| |
| 87 | | `exact` | Exact match (after strip) | |
| 88 | | `contains` | Substring match | |
| 89 | | `regex` | Regular expression | |
| 90 | | `startswith` | Prefix match | |
| 91 | | `endswith` | Suffix match | |
| 92 | |
| 93 | ### Pytest Tests |
| 94 | |
| 95 | For complex tests, use Python with pytest fixtures: |
| 96 | |
| 97 | ```python |
| 98 | import pytest |
| 99 | |
| 100 | @pytest.mark.line_editing |
| 101 | def test_ctrl_a_moves_to_beginning(fortsh): |
| 102 | fortsh.send("hello world") |
| 103 | fortsh.send_key("C-a") |
| 104 | fortsh.send("echo ") |
| 105 | fortsh.send_key("Enter") |
| 106 | output = fortsh.wait_for_prompt() |
| 107 | assert "hello world" in output |
| 108 | ``` |
| 109 | |
| 110 | #### Available Fixtures |
| 111 | |
| 112 | - `fortsh` - Running fortsh session (rc disabled) |
| 113 | - `fortsh_with_rc` - Session with user's .fortshrc |
| 114 | - `fortsh_factory` - Create multiple sessions |
| 115 | - `fortsh_path` - Path to fortsh binary |
| 116 | |
| 117 | ## Key Sequences |
| 118 | |
| 119 | Common keys are defined in `utils/keys.py`: |
| 120 | |
| 121 | ```python |
| 122 | # Control keys |
| 123 | "C-a" # Beginning of line |
| 124 | "C-e" # End of line |
| 125 | "C-k" # Kill to end |
| 126 | "C-u" # Kill to beginning |
| 127 | "C-w" # Kill word |
| 128 | "C-y" # Yank |
| 129 | "C-c" # Interrupt |
| 130 | "C-z" # Suspend |
| 131 | "C-d" # EOF/Delete |
| 132 | "C-r" # Reverse search |
| 133 | |
| 134 | # Alt/Meta keys |
| 135 | "M-b" # Back word |
| 136 | "M-f" # Forward word |
| 137 | |
| 138 | # Arrow keys |
| 139 | "Up", "Down", "Left", "Right" |
| 140 | |
| 141 | # Special |
| 142 | "Tab", "Enter", "Backspace", "Delete" |
| 143 | "Home", "End", "PageUp", "PageDown" |
| 144 | ``` |
| 145 | |
| 146 | ## Test Categories |
| 147 | |
| 148 | Tests are organized by feature (total 321 tests): |
| 149 | |
| 150 | 1. **POSIX Shell Features** (120+ tests) |
| 151 | - Basic operations |
| 152 | - Quoting and escaping |
| 153 | - Variables and expansion |
| 154 | - Pipelines and redirections |
| 155 | - Control structures (if/for/while/case) |
| 156 | - Functions and arithmetic |
| 157 | - Builtins |
| 158 | |
| 159 | 2. **Line Editing** (49 tests) |
| 160 | - Cursor movement |
| 161 | - Text modification |
| 162 | - Kill ring operations |
| 163 | - Word operations |
| 164 | |
| 165 | 3. **History** (37 tests) |
| 166 | - Arrow key navigation |
| 167 | - Ctrl+R search |
| 168 | - History expansion (!!, !$, etc.) |
| 169 | |
| 170 | 4. **Completion** (37 tests) |
| 171 | - Command completion |
| 172 | - Path/file completion |
| 173 | - Variable completion |
| 174 | |
| 175 | 5. **Signals & Job Control** (40 tests) |
| 176 | - SIGINT (Ctrl+C) |
| 177 | - SIGTSTP (Ctrl+Z) |
| 178 | - Background jobs |
| 179 | - Job specs (%n, %%, %+) |
| 180 | - fg/bg/jobs builtins |
| 181 | |
| 182 | 6. **Prompt & Display** (39 tests) |
| 183 | - PS1/PS2 escapes |
| 184 | - Terminal resize |
| 185 | - Colors and Unicode |
| 186 | |
| 187 | ## Running Specific Tests |
| 188 | |
| 189 | ```bash |
| 190 | # Run single spec file |
| 191 | python tests/interactive/run_tests.py --spec line_editing.yaml |
| 192 | |
| 193 | # Run pytest with markers |
| 194 | pytest tests/interactive -m line_editing |
| 195 | pytest tests/interactive -m "not slow" |
| 196 | |
| 197 | # Run with verbose output |
| 198 | python tests/interactive/run_tests.py -v |
| 199 | ``` |
| 200 | |
| 201 | ## Environment Variables |
| 202 | |
| 203 | - `FORTSH` - Path to fortsh binary |
| 204 | - `FORTSH_RC_FILE` - Override rc file path |
| 205 | |
| 206 | ## Adding New Tests |
| 207 | |
| 208 | ### 1. Add to existing YAML spec |
| 209 | |
| 210 | ```yaml |
| 211 | # In test_specs/line_editing.yaml |
| 212 | tests: |
| 213 | - name: "My new test" |
| 214 | steps: |
| 215 | - send_line: "echo test" |
| 216 | expect_output: "test" |
| 217 | ``` |
| 218 | |
| 219 | ### 2. Create new YAML spec |
| 220 | |
| 221 | ```bash |
| 222 | # Create tests/interactive/test_specs/my_feature.yaml |
| 223 | ``` |
| 224 | |
| 225 | ### 3. Add pytest test |
| 226 | |
| 227 | ```python |
| 228 | # Create tests/interactive/test_my_feature.py |
| 229 | |
| 230 | def test_my_feature(fortsh): |
| 231 | output = fortsh.run_command("echo hello") |
| 232 | assert "hello" in output |
| 233 | ``` |
| 234 | |
| 235 | ## CI Integration |
| 236 | |
| 237 | For GitHub Actions with tmux: |
| 238 | |
| 239 | ```yaml |
| 240 | - name: Run interactive tests |
| 241 | run: | |
| 242 | tmux new-session -d -s test |
| 243 | tmux send-keys -t test "python tests/interactive/run_tests.py" Enter |
| 244 | sleep 60 |
| 245 | tmux capture-pane -t test -p > test_output.txt |
| 246 | grep -q "ALL TESTS PASSED" test_output.txt |
| 247 | ``` |
| 248 | |
| 249 | ## Troubleshooting |
| 250 | |
| 251 | ### "fortsh binary not found" |
| 252 | |
| 253 | ```bash |
| 254 | # Build fortsh first |
| 255 | make clean && make |
| 256 | |
| 257 | # Or specify path |
| 258 | python tests/interactive/run_tests.py --fortsh /path/to/fortsh |
| 259 | ``` |
| 260 | |
| 261 | ### Test timeouts |
| 262 | |
| 263 | Increase timeout in test or PTY initialization: |
| 264 | |
| 265 | ```python |
| 266 | pty = FortshPTY(timeout=10.0) # 10 seconds |
| 267 | ``` |
| 268 | |
| 269 | ### Debugging tests |
| 270 | |
| 271 | ```python |
| 272 | # In pytest test |
| 273 | def test_debug(fortsh): |
| 274 | fortsh.send_line("echo hello") |
| 275 | import time; time.sleep(5) # Pause to observe |
| 276 | output = fortsh.wait_for_prompt() |
| 277 | print(f"Output: {output}") # Will show with -s flag |
| 278 | ``` |
| 279 | |
| 280 | Run with: |
| 281 | ```bash |
| 282 | pytest tests/interactive -s --tb=long |
| 283 | ``` |
| 284 | |
| 285 | ## Dependencies |
| 286 | |
| 287 | - Python 3.8+ |
| 288 | - pexpect >= 4.8 |
| 289 | - PyYAML >= 6.0 |
| 290 | - pytest >= 7.0 |
| 291 | - colorama >= 0.4 |
| 292 | |
| 293 | Install with: |
| 294 | ```bash |
| 295 | pip install -r tests/interactive/requirements.txt |
| 296 | ``` |