Philippe Mathieu-Daudé | c88ee46 | 2020-01-30 17:32:24 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Markus Armbruster | 9862657 | 2013-07-27 17:41:53 +0200 | [diff] [blame] | 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 | |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 14 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 15 | import argparse |
| 16 | import difflib |
| 17 | import os |
Markus Armbruster | 9862657 | 2013-07-27 17:41:53 +0200 | [diff] [blame] | 18 | import sys |
Markus Armbruster | ed39c03 | 2020-03-04 16:59:30 +0100 | [diff] [blame] | 19 | from io import StringIO |
Markus Armbruster | e6c42b9 | 2019-10-18 09:43:44 +0200 | [diff] [blame] | 20 | |
| 21 | from qapi.error import QAPIError |
| 22 | from qapi.schema import QAPISchema, QAPISchemaVisitor |
| 23 | |
Markus Armbruster | 9862657 | 2013-07-27 17:41:53 +0200 | [diff] [blame] | 24 | |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 25 | class QAPISchemaTestVisitor(QAPISchemaVisitor): |
Markus Armbruster | cf40a0a | 2018-02-11 10:35:55 +0100 | [diff] [blame] | 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 | |
Markus Armbruster | 013b4ef | 2020-03-17 12:54:37 +0100 | [diff] [blame] | 33 | def visit_enum_type(self, name, info, ifcond, features, members, prefix): |
Marc-André Lureau | 1e381b6 | 2018-12-13 16:37:05 +0400 | [diff] [blame] | 34 | print('enum %s' % name) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 35 | if prefix: |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 36 | print(' prefix %s' % prefix) |
Marc-André Lureau | 1e381b6 | 2018-12-13 16:37:05 +0400 | [diff] [blame] | 37 | for m in members: |
| 38 | print(' member %s' % m.name) |
Marc-André Lureau | 6cc32b0 | 2018-12-13 16:37:11 +0400 | [diff] [blame] | 39 | self._print_if(m.ifcond, indent=8) |
Markus Armbruster | b6c1875 | 2021-10-25 06:24:02 +0200 | [diff] [blame] | 40 | self._print_features(m.features, indent=8) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 41 | self._print_if(ifcond) |
Markus Armbruster | 013b4ef | 2020-03-17 12:54:37 +0100 | [diff] [blame] | 42 | self._print_features(features) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 43 | |
Markus Armbruster | ca0ac75 | 2019-03-01 16:40:45 +0100 | [diff] [blame] | 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 | |
Markus Armbruster | 7b3bc9e | 2020-03-17 12:54:38 +0100 | [diff] [blame] | 50 | def visit_object_type(self, name, info, ifcond, features, |
Markus Armbruster | d1da8af | 2024-03-15 16:28:22 +0100 | [diff] [blame] | 51 | base, members, branches): |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 52 | print('object %s' % name) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 53 | if base: |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 54 | print(' base %s' % base.name) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 55 | for m in members: |
Markus Armbruster | b736e25 | 2018-06-21 10:35:51 +0200 | [diff] [blame] | 56 | print(' member %s: %s optional=%s' |
| 57 | % (m.name, m.type.name, m.optional)) |
Marc-André Lureau | ccadd6b | 2018-12-13 16:37:15 +0400 | [diff] [blame] | 58 | self._print_if(m.ifcond, 8) |
Markus Armbruster | 84ab008 | 2020-03-17 12:54:45 +0100 | [diff] [blame] | 59 | self._print_features(m.features, indent=8) |
Markus Armbruster | d1da8af | 2024-03-15 16:28:22 +0100 | [diff] [blame] | 60 | self._print_variants(branches) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 61 | self._print_if(ifcond) |
Peter Krempa | 2e2e0df2 | 2019-10-18 10:14:52 +0200 | [diff] [blame] | 62 | self._print_features(features) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 63 | |
Markus Armbruster | 41d0ad1 | 2024-03-16 07:43:36 +0100 | [diff] [blame] | 64 | def visit_alternate_type(self, name, info, ifcond, features, |
| 65 | alternatives): |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 66 | print('alternate %s' % name) |
Markus Armbruster | 41d0ad1 | 2024-03-16 07:43:36 +0100 | [diff] [blame] | 67 | self._print_variants(alternatives) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 68 | self._print_if(ifcond) |
Markus Armbruster | 013b4ef | 2020-03-17 12:54:37 +0100 | [diff] [blame] | 69 | self._print_features(features) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 70 | |
Markus Armbruster | 7b3bc9e | 2020-03-17 12:54:38 +0100 | [diff] [blame] | 71 | def visit_command(self, name, info, ifcond, features, |
| 72 | arg_type, ret_type, gen, success_response, boxed, |
Kevin Wolf | 04f2236 | 2020-10-05 17:58:49 +0200 | [diff] [blame] | 73 | allow_oob, allow_preconfig, coroutine): |
Markus Armbruster | b736e25 | 2018-06-21 10:35:51 +0200 | [diff] [blame] | 74 | print('command %s %s -> %s' |
| 75 | % (name, arg_type and arg_type.name, |
| 76 | ret_type and ret_type.name)) |
Kevin Wolf | 04f2236 | 2020-10-05 17:58:49 +0200 | [diff] [blame] | 77 | print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s%s' |
| 78 | % (gen, success_response, boxed, allow_oob, allow_preconfig, |
| 79 | " coroutine=True" if coroutine else "")) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 80 | self._print_if(ifcond) |
Peter Krempa | 2e2e0df2 | 2019-10-18 10:14:52 +0200 | [diff] [blame] | 81 | self._print_features(features) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 82 | |
Markus Armbruster | 013b4ef | 2020-03-17 12:54:37 +0100 | [diff] [blame] | 83 | def visit_event(self, name, info, ifcond, features, arg_type, boxed): |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 84 | print('event %s %s' % (name, arg_type and arg_type.name)) |
Markus Armbruster | 758f272 | 2019-10-18 10:14:50 +0200 | [diff] [blame] | 85 | print(' boxed=%s' % boxed) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 86 | self._print_if(ifcond) |
Markus Armbruster | 013b4ef | 2020-03-17 12:54:37 +0100 | [diff] [blame] | 87 | self._print_features(features) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 88 | |
| 89 | @staticmethod |
| 90 | def _print_variants(variants): |
| 91 | if variants: |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 92 | print(' tag %s' % variants.tag_member.name) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 93 | for v in variants.variants: |
Daniel P. Berrange | ef9d910 | 2018-01-16 13:42:04 +0000 | [diff] [blame] | 94 | print(' case %s: %s' % (v.name, v.type.name)) |
Marc-André Lureau | a272428 | 2018-12-13 16:37:17 +0400 | [diff] [blame] | 95 | QAPISchemaTestVisitor._print_if(v.ifcond, indent=8) |
Markus Armbruster | 156402e | 2015-09-16 13:06:08 +0200 | [diff] [blame] | 96 | |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 97 | @staticmethod |
| 98 | def _print_if(ifcond, indent=4): |
Markus Armbruster | 9c629fa | 2021-08-31 14:38:07 +0200 | [diff] [blame] | 99 | # TODO Drop this hack after replacing OrderedDict by plain |
| 100 | # dict (requires Python 3.7) |
| 101 | def _massage(subcond): |
| 102 | if isinstance(subcond, str): |
| 103 | return subcond |
| 104 | if isinstance(subcond, list): |
| 105 | return [_massage(val) for val in subcond] |
| 106 | return {key: _massage(val) for key, val in subcond.items()} |
| 107 | |
Marc-André Lureau | 33aa326 | 2021-08-04 12:30:58 +0400 | [diff] [blame] | 108 | if ifcond.is_present(): |
Markus Armbruster | 9c629fa | 2021-08-31 14:38:07 +0200 | [diff] [blame] | 109 | print('%sif %s' % (' ' * indent, _massage(ifcond.ifcond))) |
Marc-André Lureau | fbf09a2 | 2018-07-03 17:56:38 +0200 | [diff] [blame] | 110 | |
Peter Krempa | 2e2e0df2 | 2019-10-18 10:14:52 +0200 | [diff] [blame] | 111 | @classmethod |
Markus Armbruster | 84ab008 | 2020-03-17 12:54:45 +0100 | [diff] [blame] | 112 | def _print_features(cls, features, indent=4): |
Peter Krempa | 2e2e0df2 | 2019-10-18 10:14:52 +0200 | [diff] [blame] | 113 | if features: |
| 114 | for f in features: |
Markus Armbruster | 84ab008 | 2020-03-17 12:54:45 +0100 | [diff] [blame] | 115 | print('%sfeature %s' % (' ' * indent, f.name)) |
| 116 | cls._print_if(f.ifcond, indent + 4) |
Peter Krempa | 2e2e0df2 | 2019-10-18 10:14:52 +0200 | [diff] [blame] | 117 | |
Markus Armbruster | 181feaf | 2018-02-11 10:35:51 +0100 | [diff] [blame] | 118 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 119 | def test_frontend(fname): |
| 120 | schema = QAPISchema(fname) |
| 121 | schema.visit(QAPISchemaTestVisitor()) |
Markus Armbruster | 181feaf | 2018-02-11 10:35:51 +0100 | [diff] [blame] | 122 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 123 | for doc in schema.docs: |
| 124 | if doc.symbol: |
| 125 | print('doc symbol=%s' % doc.symbol) |
| 126 | else: |
| 127 | print('doc freeform') |
| 128 | print(' body=\n%s' % doc.body.text) |
| 129 | for arg, section in doc.args.items(): |
| 130 | print(' arg=%s\n%s' % (arg, section.text)) |
Markus Armbruster | a0418a4 | 2019-10-24 13:02:22 +0200 | [diff] [blame] | 131 | for feat, section in doc.features.items(): |
| 132 | print(' feature=%s\n%s' % (feat, section.text)) |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 133 | for section in doc.sections: |
Markus Armbruster | 31c54b9 | 2024-02-16 15:58:32 +0100 | [diff] [blame] | 134 | print(' section=%s\n%s' % (section.tag, section.text)) |
Markus Armbruster | 818c331 | 2017-03-20 14:11:53 +0100 | [diff] [blame] | 135 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 136 | |
Markus Armbruster | f333681 | 2021-09-22 14:56:19 +0200 | [diff] [blame] | 137 | def open_test_result(dir_name, file_name, update): |
| 138 | mode = 'r+' if update else 'r' |
| 139 | try: |
Markus Armbruster | 5c24c3e | 2023-10-25 11:29:25 +0200 | [diff] [blame] | 140 | return open(os.path.join(dir_name, file_name), mode, encoding='utf-8') |
Markus Armbruster | f333681 | 2021-09-22 14:56:19 +0200 | [diff] [blame] | 141 | except FileNotFoundError: |
| 142 | if not update: |
| 143 | raise |
Markus Armbruster | 5c24c3e | 2023-10-25 11:29:25 +0200 | [diff] [blame] | 144 | return open(os.path.join(dir_name, file_name), 'w+', encoding='utf-8') |
Markus Armbruster | f333681 | 2021-09-22 14:56:19 +0200 | [diff] [blame] | 145 | |
| 146 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 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: |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 152 | errstr = str(err) + '\n' |
| 153 | if dir_name: |
| 154 | errstr = errstr.replace(dir_name + '/', '') |
| 155 | actual_err = errstr.splitlines(True) |
Markus Armbruster | 818c331 | 2017-03-20 14:11:53 +0100 | [diff] [blame] | 156 | else: |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 157 | actual_err = [] |
| 158 | finally: |
| 159 | actual_out = sys.stdout.getvalue().splitlines(True) |
| 160 | sys.stdout.close() |
| 161 | sys.stdout = sys.__stdout__ |
| 162 | |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 163 | try: |
Markus Armbruster | f333681 | 2021-09-22 14:56:19 +0200 | [diff] [blame] | 164 | outfp = open_test_result(dir_name, test_name + '.out', update) |
| 165 | errfp = open_test_result(dir_name, test_name + '.err', update) |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 166 | expected_out = outfp.readlines() |
| 167 | expected_err = errfp.readlines() |
Markus Armbruster | 436911c | 2021-09-22 14:56:18 +0200 | [diff] [blame] | 168 | except OSError as err: |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 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) |
Markus Armbruster | 436911c | 2021-09-22 14:56:18 +0200 | [diff] [blame] | 194 | except OSError as err: |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 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', |
Daniel P. Berrangé | 7ce54db | 2023-04-20 11:26:17 +0100 | [diff] [blame] | 209 | default='QAPI_TEST_UPDATE' in os.environ, |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 210 | help="update expected test results") |
| 211 | parser.add_argument('tests', nargs='*', metavar='TEST', action='store') |
| 212 | args = parser.parse_args() |
| 213 | |
| 214 | status = 0 |
| 215 | for t in args.tests: |
| 216 | (dir_name, base_name) = os.path.split(t) |
| 217 | dir_name = dir_name or args.dir |
| 218 | test_name = os.path.splitext(base_name)[0] |
| 219 | status |= test_and_diff(test_name, dir_name, args.update) |
| 220 | |
Markus Armbruster | 5c24c3e | 2023-10-25 11:29:25 +0200 | [diff] [blame] | 221 | sys.exit(status) |
Markus Armbruster | f01338c | 2019-10-18 09:43:42 +0200 | [diff] [blame] | 222 | |
| 223 | |
| 224 | if __name__ == '__main__': |
| 225 | main(sys.argv) |
Markus Armbruster | 5c24c3e | 2023-10-25 11:29:25 +0200 | [diff] [blame] | 226 | sys.exit(0) |