Rewrite the test doc generator to produce output in source order instead of arbitrarily by section name. This patch removes our last use of the "body" field, which has an effect on how the sections of each test documention block are printed. We now print the name of the section followed by the section text for all sections except Members and Features, which are printed as "Member=%s" or "Feature=%s" followed by the section text, respectively. This patch is motivated by a desire to move the QAPIDoc API away from named fields for specific sections in a bid to force all users to simply iterate through all_sections in order, instead - and to remove the named subsections. Signed-off-by: John Snow <jsnow@redhat.com> Message-ID: <20260611042332.482979-3-jsnow@redhat.com> Reviewed-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
221 lines
7.1 KiB
Python
Executable File
221 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# QAPI parser test harness
|
|
#
|
|
# Copyright (c) 2013 Red Hat Inc.
|
|
#
|
|
# Authors:
|
|
# Markus Armbruster <armbru@redhat.com>
|
|
#
|
|
# This work is licensed under the terms of the GNU GPL, version 2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
#
|
|
|
|
|
|
import argparse
|
|
import difflib
|
|
import os
|
|
import sys
|
|
from io import StringIO
|
|
|
|
from qapi.error import QAPIError
|
|
from qapi.parser import QAPIDoc
|
|
from qapi.schema import QAPISchema, QAPISchemaVisitor
|
|
|
|
|
|
class QAPISchemaTestVisitor(QAPISchemaVisitor):
|
|
|
|
def visit_module(self, name):
|
|
print('module %s' % name)
|
|
|
|
def visit_include(self, name, info):
|
|
print('include %s' % name)
|
|
|
|
def visit_enum_type(self, name, info, ifcond, features, members, prefix):
|
|
print('enum %s' % name)
|
|
if prefix:
|
|
print(' prefix %s' % prefix)
|
|
for m in members:
|
|
print(' member %s' % m.name)
|
|
self._print_if(m.ifcond, indent=8)
|
|
self._print_features(m.features, indent=8)
|
|
self._print_if(ifcond)
|
|
self._print_features(features)
|
|
|
|
def visit_array_type(self, name, info, ifcond, element_type):
|
|
if not info:
|
|
return # suppress built-in arrays
|
|
print('array %s %s' % (name, element_type.name))
|
|
self._print_if(ifcond)
|
|
|
|
def visit_object_type(self, name, info, ifcond, features,
|
|
base, members, branches):
|
|
print('object %s' % name)
|
|
if base:
|
|
print(' base %s' % base.name)
|
|
for m in members:
|
|
print(' member %s: %s optional=%s'
|
|
% (m.name, m.type.name, m.optional))
|
|
self._print_if(m.ifcond, 8)
|
|
self._print_features(m.features, indent=8)
|
|
self._print_variants(branches)
|
|
self._print_if(ifcond)
|
|
self._print_features(features)
|
|
|
|
def visit_alternate_type(self, name, info, ifcond, features,
|
|
alternatives):
|
|
print('alternate %s' % name)
|
|
self._print_variants(alternatives)
|
|
self._print_if(ifcond)
|
|
self._print_features(features)
|
|
|
|
def visit_command(self, name, info, ifcond, features,
|
|
arg_type, ret_type, gen, success_response, boxed,
|
|
allow_oob, allow_preconfig, coroutine):
|
|
print('command %s %s -> %s'
|
|
% (name, arg_type and arg_type.name,
|
|
ret_type and ret_type.name))
|
|
print(' gen=%s success_response=%s boxed=%s oob=%s preconfig=%s%s'
|
|
% (gen, success_response, boxed, allow_oob, allow_preconfig,
|
|
" coroutine=True" if coroutine else ""))
|
|
self._print_if(ifcond)
|
|
self._print_features(features)
|
|
|
|
def visit_event(self, name, info, ifcond, features, arg_type, boxed):
|
|
print('event %s %s' % (name, arg_type and arg_type.name))
|
|
print(' boxed=%s' % boxed)
|
|
self._print_if(ifcond)
|
|
self._print_features(features)
|
|
|
|
@staticmethod
|
|
def _print_variants(variants):
|
|
if variants:
|
|
print(' tag %s' % variants.tag_member.name)
|
|
for v in variants.variants:
|
|
print(' case %s: %s' % (v.name, v.type.name))
|
|
QAPISchemaTestVisitor._print_if(v.ifcond, indent=8)
|
|
|
|
@staticmethod
|
|
def _print_if(ifcond, indent=4):
|
|
if ifcond.is_present():
|
|
print('%sif %s' % (' ' * indent, ifcond.ifcond))
|
|
|
|
@classmethod
|
|
def _print_features(cls, features, indent=4):
|
|
if features:
|
|
for f in features:
|
|
print('%sfeature %s' % (' ' * indent, f.name))
|
|
cls._print_if(f.ifcond, indent + 4)
|
|
|
|
|
|
def test_frontend(fname):
|
|
schema = QAPISchema(fname)
|
|
schema.visit(QAPISchemaTestVisitor())
|
|
|
|
for doc in schema.docs:
|
|
if doc.symbol:
|
|
print('doc symbol=%s' % doc.symbol)
|
|
else:
|
|
print('doc freeform')
|
|
for section in doc.all_sections:
|
|
if isinstance(section, QAPIDoc.ArgSection):
|
|
print(' %s=%s' % (section.kind, section.name))
|
|
else:
|
|
print(' %s' % section.kind)
|
|
print(section.text)
|
|
|
|
|
|
def open_test_result(dir_name, file_name, update):
|
|
mode = 'r+' if update else 'r'
|
|
try:
|
|
return open(os.path.join(dir_name, file_name), mode, encoding='utf-8')
|
|
except FileNotFoundError:
|
|
if not update:
|
|
raise
|
|
return open(os.path.join(dir_name, file_name), 'w+', encoding='utf-8')
|
|
|
|
|
|
def test_and_diff(test_name, dir_name, update):
|
|
sys.stdout = StringIO()
|
|
try:
|
|
test_frontend(os.path.join(dir_name, test_name + '.json'))
|
|
except QAPIError as err:
|
|
errstr = str(err) + '\n'
|
|
if dir_name:
|
|
errstr = errstr.replace(dir_name + '/', '')
|
|
actual_err = errstr.splitlines(True)
|
|
else:
|
|
actual_err = []
|
|
finally:
|
|
actual_out = sys.stdout.getvalue().splitlines(True)
|
|
sys.stdout.close()
|
|
sys.stdout = sys.__stdout__
|
|
|
|
try:
|
|
outfp = open_test_result(dir_name, test_name + '.out', update)
|
|
errfp = open_test_result(dir_name, test_name + '.err', update)
|
|
expected_out = outfp.readlines()
|
|
expected_err = errfp.readlines()
|
|
except OSError as err:
|
|
print("%s: can't open '%s': %s"
|
|
% (sys.argv[0], err.filename, err.strerror),
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
if actual_out == expected_out and actual_err == expected_err:
|
|
return 0
|
|
|
|
print("%s: %s" % (test_name, 'UPDATE' if update else 'FAIL'),
|
|
file=sys.stderr)
|
|
out_diff = difflib.unified_diff(expected_out, actual_out, outfp.name)
|
|
err_diff = difflib.unified_diff(expected_err, actual_err, errfp.name)
|
|
sys.stdout.writelines(out_diff)
|
|
sys.stdout.writelines(err_diff)
|
|
|
|
if not update:
|
|
print(("\n%s: set QEMU_TEST_REGENERATE=1 to recreate reference output" +
|
|
"if the QAPI schema generator was intentionally changed") % test_name,
|
|
file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
outfp.truncate(0)
|
|
outfp.seek(0)
|
|
outfp.writelines(actual_out)
|
|
errfp.truncate(0)
|
|
errfp.seek(0)
|
|
errfp.writelines(actual_err)
|
|
except OSError as err:
|
|
print("%s: can't write '%s': %s"
|
|
% (sys.argv[0], err.filename, err.strerror),
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
return 0
|
|
|
|
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(
|
|
description='QAPI schema tester')
|
|
parser.add_argument('-d', '--dir', action='store', default='',
|
|
help="directory containing tests")
|
|
parser.add_argument('-u', '--update', action='store_true',
|
|
default='QEMU_TEST_REGENERATE' in os.environ,
|
|
help="update expected test results")
|
|
parser.add_argument('tests', nargs='*', metavar='TEST', action='store')
|
|
args = parser.parse_args()
|
|
|
|
status = 0
|
|
for t in args.tests:
|
|
(dir_name, base_name) = os.path.split(t)
|
|
dir_name = dir_name or args.dir
|
|
test_name = os.path.splitext(base_name)[0]
|
|
status |= test_and_diff(test_name, dir_name, args.update)
|
|
|
|
sys.exit(status)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|
|
sys.exit(0)
|