Python · 5580 bytes Raw Blame History
1 #!/usr/bin/env python3
2 """
3 Final cleanup pass for converted POSIX tests.
4
5 Fixes common issues like escaped variables and cleans up test format.
6 """
7
8 import yaml
9 import re
10 from pathlib import Path
11 import sys
12
13
14 def unescape_variables(command: str) -> str:
15 r"""
16 Remove backslashes before $ signs that were needed in shell scripts
17 but not needed in interactive mode.
18
19 \$VAR -> $VAR
20 \$# -> $#
21 etc.
22 """
23 # Replace \$ with $ for variable expansion
24 command = command.replace(r'\$', '$')
25 # Also handle \( \) for command substitution
26 command = command.replace(r'\(', '(')
27 command = command.replace(r'\)', ')')
28 return command
29
30
31 def fix_expected_output_for_variables(test: dict) -> dict:
32 """
33 Fix tests where the expected output is a literal $VAR but should be the value.
34 """
35 expected = test.get('expect_output', '')
36 command = test.get('steps', [{}])[0].get('send_line', '')
37
38 # If expected output is a literal variable like $VAR, $1, $#, etc.
39 # and the command sets that variable, we need to get the actual value
40 if expected.startswith('$') and not expected.startswith('$?'):
41 # Common patterns we can fix
42
43 # set -- a b c; echo $1 -> expect "a"
44 match = re.match(r'set -- ([^;]+); echo \$(\d+)', command)
45 if match:
46 args, index = match.groups()
47 arg_list = args.split()
48 idx = int(index) - 1
49 if 0 <= idx < len(arg_list):
50 test['expect_output'] = arg_list[idx]
51 return test
52
53 # set -- a b c; echo $# -> expect "3"
54 match = re.match(r'set -- ([^;]+); echo \$#', command)
55 if match:
56 args = match.group(1)
57 test['expect_output'] = str(len(args.split()))
58 return test
59
60 # set -- a b c; echo $@ -> expect "a b c"
61 match = re.match(r'set -- ([^;]+); echo \$[@\*]', command)
62 if match:
63 test['expect_output'] = match.group(1)
64 return test
65
66 # func() { echo $1 $2; }; func foo bar -> expect "foo bar"
67 match = re.match(r'func\(\) \{ echo \$(\d+) \$(\d+); \}; func (\w+) (\w+)', command)
68 if match:
69 test['expect_output'] = f"{match.group(3)} {match.group(4)}"
70 return test
71
72 # true; echo $? -> expect "0"
73 if command == 'true; echo $?':
74 test['expect_output'] = '0'
75 return test
76
77 # false; echo $? -> expect "1"
78 if command == 'false; echo $?':
79 test['expect_output'] = '1'
80 return test
81
82 return test
83
84
85 def fix_exit_code_tests(test: dict) -> dict:
86 """
87 Simplify exit code tests to just check for the exit code value.
88 """
89 if 'EXIT=' in test.get('expect_output', ''):
90 steps = test.get('steps', [])
91 if len(steps) >= 2 and 'echo "EXIT=$?"' in str(steps):
92 # Determine expected exit code from command
93 first_cmd = steps[0].get('send_line', '')
94
95 # Common patterns
96 if 'true' in first_cmd and '&&' not in first_cmd and '||' not in first_cmd:
97 test['expect_output'] = 'EXIT=0'
98 elif 'false' in first_cmd and '&&' not in first_cmd and '||' not in first_cmd:
99 test['expect_output'] = 'EXIT=1'
100 elif 'test -f' in first_cmd and 'touch' in first_cmd:
101 test['expect_output'] = 'EXIT=0'
102 elif 'return' in first_cmd:
103 # Extract return value
104 match = re.search(r'return (\d+)', first_cmd)
105 if match:
106 test['expect_output'] = f'EXIT={match.group(1)}'
107
108 # Remove note if we fixed it
109 if test.get('expect_output', '').startswith('EXIT=') and test['expect_output'] != 'EXIT=':
110 test.pop('note', None)
111
112 return test
113
114
115 def process_test(test: dict) -> dict:
116 """Apply all fixes to a single test."""
117 # Fix commands
118 for i, step in enumerate(test.get('steps', [])):
119 if 'send_line' in step:
120 step['send_line'] = unescape_variables(step['send_line'])
121
122 # Fix expected outputs
123 test = fix_expected_output_for_variables(test)
124 test = fix_exit_code_tests(test)
125
126 # Clean up auto_fixed marker if present
127 # (keep it for documentation but it's not needed)
128
129 return test
130
131
132 def main():
133 if len(sys.argv) < 2:
134 print(f"Usage: {sys.argv[0]} <yaml_file>")
135 sys.exit(1)
136
137 yaml_file = Path(sys.argv[1])
138 if not yaml_file.exists():
139 print(f"Error: File not found: {yaml_file}")
140 sys.exit(1)
141
142 # Load
143 with open(yaml_file, 'r') as f:
144 data = yaml.safe_load(f)
145
146 # Count before
147 before_manual = sum(1 for t in data['tests']
148 if 'MANUAL_REVIEW' in str(t.get('expect_output', '')) or
149 t.get('expect_output', '') == '$VAR' or
150 t.get('expect_output', '') == '$1')
151
152 # Process
153 for i, test in enumerate(data['tests']):
154 data['tests'][i] = process_test(test)
155
156 # Count after
157 after_manual = sum(1 for t in data['tests']
158 if 'MANUAL_REVIEW' in str(t.get('expect_output', '')) or
159 'note' in t)
160
161 fixed = before_manual - after_manual
162
163 # Write
164 with open(yaml_file, 'w') as f:
165 yaml.dump(data, f, default_flow_style=False, sort_keys=False)
166
167 print(f"✓ Finalized {len(data['tests'])} tests")
168 print(f" Additional fixes applied: {fixed}")
169 print(f" Still needs review: {after_manual}")
170
171
172 if __name__ == '__main__':
173 main()