| 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) |