Python · 7233 bytes Raw Blame History
1 #!/usr/bin/env python3
2 """
3 Convert non-interactive POSIX compliance tests to interactive YAML format.
4
5 This tool parses posix_compliance*.sh files and generates YAML test specifications
6 that can be run with the interactive test framework.
7 """
8
9 import re
10 import sys
11 import yaml
12 from pathlib import Path
13 from typing import List, Dict, Any, Optional
14
15
16 class POSIXTestConverter:
17 """Convert POSIX shell tests to interactive YAML format."""
18
19 def __init__(self):
20 self.current_section = None
21 self.tests = []
22
23 def parse_section(self, line: str) -> Optional[str]:
24 """Parse a section declaration."""
25 match = re.match(r'section\s+"([^"]+)"', line)
26 if match:
27 return match.group(1)
28 return None
29
30 def parse_compare_posix_output(self, line: str) -> Optional[Dict[str, Any]]:
31 """
32 Parse: compare_posix_output "test name" "command"
33
34 Returns a test dictionary ready for YAML output.
35 """
36 # Handle both single and double quoted commands
37 match = re.match(r'compare_posix_output\s+"([^"]+)"\s+"([^"]+)"', line)
38 if not match:
39 match = re.match(r"compare_posix_output\s+'([^']+)'\s+'([^']+)'", line)
40 if not match:
41 # Try mixed quotes
42 match = re.match(r'compare_posix_output\s+"([^"]+)"\s+\'([^\']+)\'', line)
43
44 if match:
45 name, command = match.groups()
46
47 # Estimate expected output based on command
48 expected_output = self._estimate_output(command)
49
50 return {
51 'name': f"{self.current_section}: {name}" if self.current_section else name,
52 'steps': [{'send_line': command}],
53 'expect_output': expected_output,
54 'match_type': 'contains'
55 }
56
57 return None
58
59 def parse_compare_posix_exit_code(self, line: str) -> Optional[Dict[str, Any]]:
60 """
61 Parse: compare_posix_exit_code "test name" "command"
62
63 Exit code tests are harder in interactive mode - we need to check $?
64 """
65 match = re.match(r'compare_posix_exit_code\s+"([^"]+)"\s+"([^"]+)"', line)
66 if match:
67 name, command = match.groups()
68
69 # Add step to check exit code
70 return {
71 'name': f"{self.current_section}: {name} (exit code)" if self.current_section else f"{name} (exit code)",
72 'steps': [
73 {'send_line': command},
74 {'send_line': 'echo "EXIT=$?"'}
75 ],
76 'expect_output': 'EXIT=', # Will need manual adjustment
77 'match_type': 'contains',
78 'note': 'MANUAL REVIEW NEEDED: Check expected exit code'
79 }
80
81 return None
82
83 def _estimate_output(self, command: str) -> str:
84 """
85 Estimate the expected output of a command.
86
87 This is a best-effort heuristic and may need manual review.
88 """
89 # Simple echo commands
90 if command.startswith('echo '):
91 # Extract what's being echoed
92 echo_match = re.match(r'echo\s+(.+)', command)
93 if echo_match:
94 content = echo_match.group(1)
95 # Remove quotes if present
96 content = content.strip('\'"')
97 # Handle variable expansion markers
98 if '$' in content:
99 return 'MANUAL_REVIEW' # Variables need manual check
100 return content
101
102 # printf commands
103 if command.startswith('printf '):
104 return 'MANUAL_REVIEW' # printf is complex
105
106 # Variable assignments followed by echo
107 if ';' in command:
108 parts = command.split(';')
109 last_part = parts[-1].strip()
110 if last_part.startswith('echo '):
111 return self._estimate_output(last_part)
112
113 # Commands that typically produce numeric output
114 if 'wc -l' in command or 'grep -c' in command:
115 return 'DIGIT' # Will match any digit
116
117 # Default: mark for manual review
118 return 'MANUAL_REVIEW'
119
120 def parse_file(self, file_path: Path) -> List[Dict[str, Any]]:
121 """Parse a POSIX compliance test file and extract tests."""
122 self.tests = []
123
124 with open(file_path, 'r') as f:
125 for line in f:
126 line = line.strip()
127
128 # Check for section
129 section = self.parse_section(line)
130 if section:
131 self.current_section = section
132 continue
133
134 # Check for compare_posix_output
135 test = self.parse_compare_posix_output(line)
136 if test:
137 self.tests.append(test)
138 continue
139
140 # Check for compare_posix_exit_code
141 test = self.parse_compare_posix_exit_code(line)
142 if test:
143 self.tests.append(test)
144 continue
145
146 return self.tests
147
148 def generate_yaml(self, tests: List[Dict[str, Any]], category: str) -> str:
149 """Generate YAML output for tests."""
150 output = {
151 'metadata': {
152 'category': category,
153 'description': f'Tests converted from POSIX compliance suite',
154 'auto_generated': True,
155 'needs_review': 'Tests with MANUAL_REVIEW need expected output verification'
156 },
157 'tests': tests
158 }
159
160 return yaml.dump(output, default_flow_style=False, sort_keys=False)
161
162
163 def main():
164 """Main entry point."""
165 if len(sys.argv) < 2:
166 print(f"Usage: {sys.argv[0]} <posix_compliance_file.sh> [output.yaml]")
167 print("\nExample:")
168 print(f" {sys.argv[0]} ../../fortsh/tests/posix_compliance_gaps.sh gaps_interactive.yaml")
169 sys.exit(1)
170
171 input_file = Path(sys.argv[1])
172 if not input_file.exists():
173 print(f"Error: File not found: {input_file}")
174 sys.exit(1)
175
176 output_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None
177
178 # Determine category from filename
179 category = input_file.stem.replace('posix_compliance_', '').replace('_', ' ').title()
180
181 # Convert tests
182 converter = POSIXTestConverter()
183 tests = converter.parse_file(input_file)
184
185 print(f"Parsed {len(tests)} tests from {input_file}")
186
187 # Count tests needing review
188 needs_review = sum(1 for t in tests if
189 'MANUAL_REVIEW' in str(t.get('expect_output', '')) or
190 'note' in t)
191
192 if needs_review > 0:
193 print(f"⚠️ {needs_review} tests need manual review of expected output")
194
195 # Generate YAML
196 yaml_content = converter.generate_yaml(tests, category)
197
198 if output_file:
199 output_file.write_text(yaml_content)
200 print(f"✓ Wrote YAML to {output_file}")
201 else:
202 print("\n" + "="*60)
203 print("Generated YAML:")
204 print("="*60)
205 print(yaml_content)
206
207 # Print statistics
208 print(f"\nStatistics:")
209 print(f" Total tests: {len(tests)}")
210 print(f" Needs review: {needs_review}")
211 print(f" Ready to use: {len(tests) - needs_review}")
212
213
214 if __name__ == '__main__':
215 main()