master
py 220 lines 7.11 KB
Raw
1 #!/usr/bin/env python3
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
14
15 import argparse
16 import difflib
17 import os
18 import sys
19 from io import StringIO
20
21 from qapi.error import QAPIError
22 from qapi.parser import QAPIDoc
23 from qapi.schema import QAPISchema, QAPISchemaVisitor
24
25
26 class QAPISchemaTestVisitor(QAPISchemaVisitor):
27
28 def visit_module(self, name):
29 print('module %s' % name)
30
31 def visit_include(self, name, info):
32 print('include %s' % name)
33
34 def visit_enum_type(self, name, info, ifcond, features, members, prefix):
35 print('enum %s' % name)
36 if prefix:
37 print(' prefix %s' % prefix)
38 for m in members:
39 print(' member %s' % m.name)
40 self._print_if(m.ifcond, indent=8)
41 self._print_features(m.features, indent=8)
42 self._print_if(ifcond)
43 self._print_features(features)
44
45 def visit_array_type(self, name, info, ifcond, element_type):
46 if not info:
47 return # suppress built-in arrays
48 print('array %s %s' % (name, element_type.name))
49 self._print_if(ifcond)
50
51 def visit_object_type(self, name, info, ifcond, features,
52 base, members, branches):
53 print('object %s' % name)
54 if base:
55 print(' base %s' % base.name)
56 for m in members:
57 print(' member %s: %s optional=%s'
58 % (m.name, m.type.name, m.optional))
59 self._print_if(m.ifcond, 8)
60 self._print_features(m.features, indent=8)
61 self._print_variants(branches)
62 self._print_if(ifcond)
63 self._print_features(features)
64
65 def visit_alternate_type(self, name, info, ifcond, features,
66 alternatives):
67 print('alternate %s' % name)
68 self._print_variants(alternatives)
69 self._print_if(ifcond)
70 self._print_features(features)
71
72 def visit_command(self, name, info, ifcond, features,
73 arg_type, ret_type, gen, success_response, boxed,
74 allow_oob, allow_preconfig, coroutine):
75 print('command %s %s -> %s'
76 % (name, arg_type and arg_type.name,
77 ret_type and ret_type.name))
78 print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s%s'
79 % (gen, success_response, boxed, allow_oob, allow_preconfig,
80 " coroutine=True" if coroutine else ""))
81 self._print_if(ifcond)
82 self._print_features(features)
83
84 def visit_event(self, name, info, ifcond, features, arg_type, boxed):
85 print('event %s %s' % (name, arg_type and arg_type.name))
86 print(' boxed=%s' % boxed)
87 self._print_if(ifcond)
88 self._print_features(features)
89
90 @staticmethod
91 def _print_variants(variants):
92 if variants:
93 print(' tag %s' % variants.tag_member.name)
94 for v in variants.variants:
95 print(' case %s: %s' % (v.name, v.type.name))
96 QAPISchemaTestVisitor._print_if(v.ifcond, indent=8)
97
98 @staticmethod
99 def _print_if(ifcond, indent=4):
100 if ifcond.is_present():
101 print('%sif %s' % (' ' * indent, ifcond.ifcond))
102
103 @classmethod
104 def _print_features(cls, features, indent=4):
105 if features:
106 for f in features:
107 print('%sfeature %s' % (' ' * indent, f.name))
108 cls._print_if(f.ifcond, indent + 4)
109
110
111 def test_frontend(fname):
112 schema = QAPISchema(fname)
113 schema.visit(QAPISchemaTestVisitor())
114
115 for doc in schema.docs:
116 if doc.symbol:
117 print('doc symbol=%s' % doc.symbol)
118 else:
119 print('doc freeform')
120 for section in doc.all_sections:
121 if isinstance(section, QAPIDoc.ArgSection):
122 print(' %s=%s' % (section.kind, section.name))
123 else:
124 print(' %s' % section.kind)
125 print(section.text)
126
127
128 def open_test_result(dir_name, file_name, update):
129 mode = 'r+' if update else 'r'
130 try:
131 return open(os.path.join(dir_name, file_name), mode, encoding='utf-8')
132 except FileNotFoundError:
133 if not update:
134 raise
135 return open(os.path.join(dir_name, file_name), 'w+', encoding='utf-8')
136
137
138 def test_and_diff(test_name, dir_name, update):
139 sys.stdout = StringIO()
140 try:
141 test_frontend(os.path.join(dir_name, test_name + '.json'))
142 except QAPIError as err:
143 errstr = str(err) + '\n'
144 if dir_name:
145 errstr = errstr.replace(dir_name + '/', '')
146 actual_err = errstr.splitlines(True)
147 else:
148 actual_err = []
149 finally:
150 actual_out = sys.stdout.getvalue().splitlines(True)
151 sys.stdout.close()
152 sys.stdout = sys.__stdout__
153
154 try:
155 outfp = open_test_result(dir_name, test_name + '.out', update)
156 errfp = open_test_result(dir_name, test_name + '.err', update)
157 expected_out = outfp.readlines()
158 expected_err = errfp.readlines()
159 except OSError as err:
160 print("%s: can't open '%s': %s"
161 % (sys.argv[0], err.filename, err.strerror),
162 file=sys.stderr)
163 return 2
164
165 if actual_out == expected_out and actual_err == expected_err:
166 return 0
167
168 print("%s: %s" % (test_name, 'UPDATE' if update else 'FAIL'),
169 file=sys.stderr)
170 out_diff = difflib.unified_diff(expected_out, actual_out, outfp.name)
171 err_diff = difflib.unified_diff(expected_err, actual_err, errfp.name)
172 sys.stdout.writelines(out_diff)
173 sys.stdout.writelines(err_diff)
174
175 if not update:
176 print(("\n%s: set QEMU_TEST_REGENERATE=1 to recreate reference output" +
177 "if the QAPI schema generator was intentionally changed") % test_name,
178 file=sys.stderr)
179 return 1
180
181 try:
182 outfp.truncate(0)
183 outfp.seek(0)
184 outfp.writelines(actual_out)
185 errfp.truncate(0)
186 errfp.seek(0)
187 errfp.writelines(actual_err)
188 except OSError as err:
189 print("%s: can't write '%s': %s"
190 % (sys.argv[0], err.filename, err.strerror),
191 file=sys.stderr)
192 return 2
193
194 return 0
195
196
197 def main(argv):
198 parser = argparse.ArgumentParser(
199 description='QAPI schema tester')
200 parser.add_argument('-d', '--dir', action='store', default='',
201 help="directory containing tests")
202 parser.add_argument('-u', '--update', action='store_true',
203 default='QEMU_TEST_REGENERATE' in os.environ,
204 help="update expected test results")
205 parser.add_argument('tests', nargs='*', metavar='TEST', action='store')
206 args = parser.parse_args()
207
208 status = 0
209 for t in args.tests:
210 (dir_name, base_name) = os.path.split(t)
211 dir_name = dir_name or args.dir
212 test_name = os.path.splitext(base_name)[0]
213 status |= test_and_diff(test_name, dir_name, args.update)
214
215 sys.exit(status)
216
217
218 if __name__ == '__main__':
219 main(sys.argv)
220 sys.exit(0)