| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | Fix exit code tests by running commands in /bin/sh and capturing actual exit codes. |
| 4 | """ |
| 5 | |
| 6 | import yaml |
| 7 | import subprocess |
| 8 | import sys |
| 9 | from pathlib import Path |
| 10 | |
| 11 | |
| 12 | def get_exit_code(command: str) -> int: |
| 13 | """Run command in /bin/sh and return exit code.""" |
| 14 | try: |
| 15 | result = subprocess.run( |
| 16 | ['/bin/sh', '-c', command], |
| 17 | capture_output=True, |
| 18 | text=True, |
| 19 | timeout=5 |
| 20 | ) |
| 21 | return result.returncode |
| 22 | except Exception as e: |
| 23 | print(f" ⚠️ Failed to run: {command[:50]}... - {e}") |
| 24 | return None |
| 25 | |
| 26 | |
| 27 | def fix_exit_code_test(test: dict) -> tuple[dict, bool]: |
| 28 | """ |
| 29 | Fix a single exit code test. |
| 30 | Returns (updated_test, was_fixed) |
| 31 | """ |
| 32 | # Check if this is an exit code test that needs review |
| 33 | if test.get('note') != 'MANUAL REVIEW NEEDED: Check expected exit code': |
| 34 | return test, False |
| 35 | |
| 36 | steps = test.get('steps', []) |
| 37 | if len(steps) < 2: |
| 38 | return test, False |
| 39 | |
| 40 | # Last step should be: echo "EXIT=$?" |
| 41 | last_step = steps[-1] |
| 42 | if not last_step.get('send_line', '').startswith('echo "EXIT=$?"'): |
| 43 | return test, False |
| 44 | |
| 45 | # Expect output should be EXIT= |
| 46 | if test.get('expect_output') != 'EXIT=': |
| 47 | return test, False |
| 48 | |
| 49 | # Get all commands before the echo |
| 50 | commands = [] |
| 51 | for step in steps[:-1]: |
| 52 | cmd = step.get('send_line', '') |
| 53 | if cmd: |
| 54 | commands.append(cmd) |
| 55 | |
| 56 | # Combine commands with ; and run to get exit code |
| 57 | full_command = '; '.join(commands) |
| 58 | exit_code = get_exit_code(full_command) |
| 59 | |
| 60 | if exit_code is None: |
| 61 | return test, False |
| 62 | |
| 63 | # Update the test |
| 64 | test['expect_output'] = f'EXIT={exit_code}' |
| 65 | test.pop('note', None) # Remove the manual review note |
| 66 | |
| 67 | return test, True |
| 68 | |
| 69 | |
| 70 | def main(): |
| 71 | if len(sys.argv) < 2: |
| 72 | print(f"Usage: {sys.argv[0]} <yaml_file>") |
| 73 | sys.exit(1) |
| 74 | |
| 75 | yaml_file = Path(sys.argv[1]) |
| 76 | if not yaml_file.exists(): |
| 77 | print(f"Error: File not found: {yaml_file}") |
| 78 | sys.exit(1) |
| 79 | |
| 80 | # Load YAML |
| 81 | with open(yaml_file, 'r') as f: |
| 82 | data = yaml.safe_load(f) |
| 83 | |
| 84 | # Count before |
| 85 | before_count = sum(1 for t in data['tests'] |
| 86 | if t.get('note') == 'MANUAL REVIEW NEEDED: Check expected exit code') |
| 87 | |
| 88 | # Fix tests |
| 89 | fixed_count = 0 |
| 90 | for i, test in enumerate(data['tests']): |
| 91 | updated_test, was_fixed = fix_exit_code_test(test) |
| 92 | data['tests'][i] = updated_test |
| 93 | if was_fixed: |
| 94 | fixed_count += 1 |
| 95 | # Print progress |
| 96 | if fixed_count % 10 == 0: |
| 97 | print(f" Fixed {fixed_count} tests...") |
| 98 | |
| 99 | # Write back |
| 100 | with open(yaml_file, 'w') as f: |
| 101 | yaml.dump(data, f, default_flow_style=False, sort_keys=False) |
| 102 | |
| 103 | print(f"\n✓ Fixed {fixed_count}/{before_count} exit code tests") |
| 104 | print(f" File: {yaml_file}") |
| 105 | |
| 106 | remaining = before_count - fixed_count |
| 107 | if remaining > 0: |
| 108 | print(f"\n⚠️ {remaining} exit code tests still need manual review") |
| 109 | |
| 110 | |
| 111 | if __name__ == '__main__': |
| 112 | main() |