]>
Commit | Line | Data |
---|---|---|
c88ee46c | 1 | #!/usr/bin/env python3 |
98626572 MA |
2 | # |
3 | # QAPI parser test harness | |
4 | # | |
5 | # Copyright (c) 2013 Red Hat Inc. | |
6 | # | |
7 | # Authors: | |
8 | # Markus Armbruster <armbru@redhat.com> | |
9 | # | |
10 | # This work is licensed under the terms of the GNU GPL, version 2 or later. | |
11 | # See the COPYING file in the top-level directory. | |
12 | # | |
13 | ||
e6c42b96 | 14 | |
f01338cc MA |
15 | import argparse |
16 | import difflib | |
17 | import os | |
98626572 | 18 | import sys |
ed39c03e | 19 | from io import StringIO |
e6c42b96 MA |
20 | |
21 | from qapi.error import QAPIError | |
22 | from qapi.schema import QAPISchema, QAPISchemaVisitor | |
23 | ||
156402e5 MA |
24 | |
25 | class QAPISchemaTestVisitor(QAPISchemaVisitor): | |
cf40a0a5 MA |
26 | |
27 | def visit_module(self, name): | |
28 | print('module %s' % name) | |
29 | ||
30 | def visit_include(self, name, info): | |
31 | print('include %s' % name) | |
32 | ||
013b4efc | 33 | def visit_enum_type(self, name, info, ifcond, features, members, prefix): |
1e381b65 | 34 | print('enum %s' % name) |
156402e5 | 35 | if prefix: |
ef9d9108 | 36 | print(' prefix %s' % prefix) |
1e381b65 MAL |
37 | for m in members: |
38 | print(' member %s' % m.name) | |
6cc32b0e | 39 | self._print_if(m.ifcond, indent=8) |
b6c18755 | 40 | self._print_features(m.features, indent=8) |
fbf09a2f | 41 | self._print_if(ifcond) |
013b4efc | 42 | self._print_features(features) |
156402e5 | 43 | |
ca0ac758 MA |
44 | def visit_array_type(self, name, info, ifcond, element_type): |
45 | if not info: | |
46 | return # suppress built-in arrays | |
47 | print('array %s %s' % (name, element_type.name)) | |
48 | self._print_if(ifcond) | |
49 | ||
7b3bc9e2 MA |
50 | def visit_object_type(self, name, info, ifcond, features, |
51 | base, members, variants): | |
ef9d9108 | 52 | print('object %s' % name) |
156402e5 | 53 | if base: |
ef9d9108 | 54 | print(' base %s' % base.name) |
156402e5 | 55 | for m in members: |
b736e25a MA |
56 | print(' member %s: %s optional=%s' |
57 | % (m.name, m.type.name, m.optional)) | |
ccadd6bc | 58 | self._print_if(m.ifcond, 8) |
84ab0086 | 59 | self._print_features(m.features, indent=8) |
156402e5 | 60 | self._print_variants(variants) |
fbf09a2f | 61 | self._print_if(ifcond) |
2e2e0df2 | 62 | self._print_features(features) |
156402e5 | 63 | |
013b4efc | 64 | def visit_alternate_type(self, name, info, ifcond, features, variants): |
ef9d9108 | 65 | print('alternate %s' % name) |
156402e5 | 66 | self._print_variants(variants) |
fbf09a2f | 67 | self._print_if(ifcond) |
013b4efc | 68 | self._print_features(features) |
156402e5 | 69 | |
7b3bc9e2 MA |
70 | def visit_command(self, name, info, ifcond, features, |
71 | arg_type, ret_type, gen, success_response, boxed, | |
04f22362 | 72 | allow_oob, allow_preconfig, coroutine): |
b736e25a MA |
73 | print('command %s %s -> %s' |
74 | % (name, arg_type and arg_type.name, | |
75 | ret_type and ret_type.name)) | |
04f22362 KW |
76 | print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s%s' |
77 | % (gen, success_response, boxed, allow_oob, allow_preconfig, | |
78 | " coroutine=True" if coroutine else "")) | |
fbf09a2f | 79 | self._print_if(ifcond) |
2e2e0df2 | 80 | self._print_features(features) |
156402e5 | 81 | |
013b4efc | 82 | def visit_event(self, name, info, ifcond, features, arg_type, boxed): |
ef9d9108 | 83 | print('event %s %s' % (name, arg_type and arg_type.name)) |
758f272b | 84 | print(' boxed=%s' % boxed) |
fbf09a2f | 85 | self._print_if(ifcond) |
013b4efc | 86 | self._print_features(features) |
156402e5 MA |
87 | |
88 | @staticmethod | |
89 | def _print_variants(variants): | |
90 | if variants: | |
ef9d9108 | 91 | print(' tag %s' % variants.tag_member.name) |
156402e5 | 92 | for v in variants.variants: |
ef9d9108 | 93 | print(' case %s: %s' % (v.name, v.type.name)) |
a2724280 | 94 | QAPISchemaTestVisitor._print_if(v.ifcond, indent=8) |
156402e5 | 95 | |
fbf09a2f MAL |
96 | @staticmethod |
97 | def _print_if(ifcond, indent=4): | |
9c629fa8 MA |
98 | # TODO Drop this hack after replacing OrderedDict by plain |
99 | # dict (requires Python 3.7) | |
100 | def _massage(subcond): | |
101 | if isinstance(subcond, str): | |
102 | return subcond | |
103 | if isinstance(subcond, list): | |
104 | return [_massage(val) for val in subcond] | |
105 | return {key: _massage(val) for key, val in subcond.items()} | |
106 | ||
33aa3267 | 107 | if ifcond.is_present(): |
9c629fa8 | 108 | print('%sif %s' % (' ' * indent, _massage(ifcond.ifcond))) |
fbf09a2f | 109 | |
2e2e0df2 | 110 | @classmethod |
84ab0086 | 111 | def _print_features(cls, features, indent=4): |
2e2e0df2 PK |
112 | if features: |
113 | for f in features: | |
84ab0086 MA |
114 | print('%sfeature %s' % (' ' * indent, f.name)) |
115 | cls._print_if(f.ifcond, indent + 4) | |
2e2e0df2 | 116 | |
181feaf3 | 117 | |
f01338cc MA |
118 | def test_frontend(fname): |
119 | schema = QAPISchema(fname) | |
120 | schema.visit(QAPISchemaTestVisitor()) | |
121 | ||
122 | for doc in schema.docs: | |
123 | if doc.symbol: | |
124 | print('doc symbol=%s' % doc.symbol) | |
125 | else: | |
126 | print('doc freeform') | |
127 | print(' body=\n%s' % doc.body.text) | |
128 | for arg, section in doc.args.items(): | |
129 | print(' arg=%s\n%s' % (arg, section.text)) | |
a0418a4a MA |
130 | for feat, section in doc.features.items(): |
131 | print(' feature=%s\n%s' % (feat, section.text)) | |
f01338cc MA |
132 | for section in doc.sections: |
133 | print(' section=%s\n%s' % (section.name, section.text)) | |
134 | ||
135 | ||
f333681c MA |
136 | def open_test_result(dir_name, file_name, update): |
137 | mode = 'r+' if update else 'r' | |
138 | try: | |
139 | fp = open(os.path.join(dir_name, file_name), mode) | |
140 | except FileNotFoundError: | |
141 | if not update: | |
142 | raise | |
143 | fp = open(os.path.join(dir_name, file_name), 'w+') | |
144 | return fp | |
145 | ||
146 | ||
f01338cc MA |
147 | def test_and_diff(test_name, dir_name, update): |
148 | sys.stdout = StringIO() | |
149 | try: | |
150 | test_frontend(os.path.join(dir_name, test_name + '.json')) | |
151 | except QAPIError as err: | |
f01338cc MA |
152 | errstr = str(err) + '\n' |
153 | if dir_name: | |
154 | errstr = errstr.replace(dir_name + '/', '') | |
155 | actual_err = errstr.splitlines(True) | |
818c3318 | 156 | else: |
f01338cc MA |
157 | actual_err = [] |
158 | finally: | |
159 | actual_out = sys.stdout.getvalue().splitlines(True) | |
160 | sys.stdout.close() | |
161 | sys.stdout = sys.__stdout__ | |
162 | ||
f01338cc | 163 | try: |
f333681c MA |
164 | outfp = open_test_result(dir_name, test_name + '.out', update) |
165 | errfp = open_test_result(dir_name, test_name + '.err', update) | |
f01338cc MA |
166 | expected_out = outfp.readlines() |
167 | expected_err = errfp.readlines() | |
436911c2 | 168 | except OSError as err: |
f01338cc MA |
169 | print("%s: can't open '%s': %s" |
170 | % (sys.argv[0], err.filename, err.strerror), | |
171 | file=sys.stderr) | |
172 | return 2 | |
173 | ||
174 | if actual_out == expected_out and actual_err == expected_err: | |
175 | return 0 | |
176 | ||
177 | print("%s %s" % (test_name, 'UPDATE' if update else 'FAIL'), | |
178 | file=sys.stderr) | |
179 | out_diff = difflib.unified_diff(expected_out, actual_out, outfp.name) | |
180 | err_diff = difflib.unified_diff(expected_err, actual_err, errfp.name) | |
181 | sys.stdout.writelines(out_diff) | |
182 | sys.stdout.writelines(err_diff) | |
183 | ||
184 | if not update: | |
185 | return 1 | |
186 | ||
187 | try: | |
188 | outfp.truncate(0) | |
189 | outfp.seek(0) | |
190 | outfp.writelines(actual_out) | |
191 | errfp.truncate(0) | |
192 | errfp.seek(0) | |
193 | errfp.writelines(actual_err) | |
436911c2 | 194 | except OSError as err: |
f01338cc MA |
195 | print("%s: can't write '%s': %s" |
196 | % (sys.argv[0], err.filename, err.strerror), | |
197 | file=sys.stderr) | |
198 | return 2 | |
199 | ||
200 | return 0 | |
201 | ||
202 | ||
203 | def main(argv): | |
204 | parser = argparse.ArgumentParser( | |
205 | description='QAPI schema tester') | |
206 | parser.add_argument('-d', '--dir', action='store', default='', | |
207 | help="directory containing tests") | |
208 | parser.add_argument('-u', '--update', action='store_true', | |
209 | help="update expected test results") | |
210 | parser.add_argument('tests', nargs='*', metavar='TEST', action='store') | |
211 | args = parser.parse_args() | |
212 | ||
213 | status = 0 | |
214 | for t in args.tests: | |
215 | (dir_name, base_name) = os.path.split(t) | |
216 | dir_name = dir_name or args.dir | |
217 | test_name = os.path.splitext(base_name)[0] | |
218 | status |= test_and_diff(test_name, dir_name, args.update) | |
219 | ||
220 | exit(status) | |
221 | ||
222 | ||
223 | if __name__ == '__main__': | |
224 | main(sys.argv) | |
225 | exit(0) |