[PATCH] scripts: QMP schema query string helper script

Peter Krempa posted 1 patch 11 months, 2 weeks ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/libvirt tags/patchew/f849543460e8b4bab49ec5b479ae374508c45415.1686150525.git.pkrempa@redhat.com
scripts/meson.build             |   1 +
scripts/qapi-schema-diff-gen.py | 313 ++++++++++++++++++++++++++++++++
tests/meson.build               |  12 ++
3 files changed, 326 insertions(+)
create mode 100755 scripts/qapi-schema-diff-gen.py
[PATCH] scripts: QMP schema query string helper script
Posted by Peter Krempa 11 months, 2 weeks ago
The script generates all query strings in same format as used for
querying qemu capabilities for a given .replies file.

The output can be used to either aid in creation of the query strings as
well as to generate diff between the schema, which is useful when adding
a new capability dump.

The script also validates that all of the schema is supported by our
tools so that we can always adapt.

The output looks like:

 $ ./scripts/qapi-schema-diff-gen.py tests/qemucapabilitiesdata/caps_8.0.0_x86_64.replies
 [...]
 query-yank
 query-yank/ret-type/type
 query-yank/ret-type/type/^block-node
 query-yank/ret-type/type/^chardev
 query-yank/ret-type/type/^migration
 query-yank/ret-type/+block-node
 query-yank/ret-type/+block-node/node-name
 query-yank/ret-type/+block-node/node-name/!str
 query-yank/ret-type/+chardev
 query-yank/ret-type/+chardev/id
 query-yank/ret-type/+chardev/id/!str
 query-yank/ret-type/+migration
 [...]

Signed-off-by: Peter Krempa <pkrempa@redhat.com>
---
 scripts/meson.build             |   1 +
 scripts/qapi-schema-diff-gen.py | 313 ++++++++++++++++++++++++++++++++
 tests/meson.build               |  12 ++
 3 files changed, 326 insertions(+)
 create mode 100755 scripts/qapi-schema-diff-gen.py

diff --git a/scripts/meson.build b/scripts/meson.build
index 05b71184f1..c216c7e1ff 100644
--- a/scripts/meson.build
+++ b/scripts/meson.build
@@ -30,6 +30,7 @@ scripts = [
   'meson-timestamp.py',
   'mock-noinline.py',
   'prohibit-duplicate-header.py',
+  'qapi-schema-diff-gen.py',
 ]

 foreach name : scripts
diff --git a/scripts/qapi-schema-diff-gen.py b/scripts/qapi-schema-diff-gen.py
new file mode 100755
index 0000000000..bde130e33c
--- /dev/null
+++ b/scripts/qapi-schema-diff-gen.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# A tool to help with creating query strings for querying the QMP schema (as
+# returned by 'query-qmp-schema' QMP command). In default mode it generates all
+# the possible query strings for a QMP schema in the ".replies" format as
+# generated by 'tests/qemucapsprobe.c'. This can be either used by users to
+# find the desired schema query string or to see what changed between two
+# versions.
+#
+# In the '--validate' mode the script doesn't output the schema query strings.
+# This invokes just the validator that everything in the schema is supported by
+# the tool.
+#
+# Note: Any change to the 'validate_schema' function below to make it accept
+# new schema components most likely requires change to either
+# 'src/qemu/qemu_qapi.c' or 'tests/testutilsqemuschema.c' to accept the new
+# schema components.
+
+from pathlib import Path
+import argparse
+import json
+import sys
+
+
+# Finds the apropriate call to 'query-qmp-schema' in the '.replies' file and
+# returns the JSON blob following the command invocation.
+def load_schema_json_list(filename):
+    found = False
+
+    with open(filename, "r") as fh:
+        jsonstr = ''
+        for line in fh:
+            jsonstr += line
+
+            if line != '}\n':
+                continue
+
+            if found:
+                return json.loads(jsonstr)["return"]
+
+            cmdobj = json.loads(jsonstr)
+            jsonstr = ""
+
+            if isinstance(cmdobj, dict) and cmdobj.get('execute', '') == 'query-qmp-schema':
+                found = True
+
+
+# Validates that 'entry' (an member of the QMP schema):
+# - checks that it's a Dict (imported from a JSON object)
+# - checks that all 'mandatory' fields are present and their types match
+# - checks the types of all 'optional' fields
+# - checks that no unknown fields are present
+def check_keys(entry, mandatory, optional):
+    keys = set(entry.keys())
+
+    for k, t in mandatory:
+        try:
+            keys.remove(k)
+        except KeyError:
+            raise Exception("missing mandatory key '%s' in schema '%s'" % (k, entry))
+
+        if not isinstance(entry[k], t):
+            raise Exception("key '%s' is not of the expected type '%s' in schema '%s'" % (k, t, entry))
+
+    for k, t in optional:
+        if k in keys:
+            keys.discard(k)
+
+            if t is not None:
+                if not isinstance(entry[k], t):
+                    raise Exception("key '%s' is not of the expected type '%s' in schema '%s'" % (k, t, entry))
+
+    if len(keys) > 0:
+        raise Exception("unhandled keys '%s' in schema '%s'" % (','.join(list(keys)), entry))
+
+
+# Validates the optional 'features' and that they consist only of strings
+def check_features_list(entry):
+    for f in entry.get('features', []):
+        if not isinstance(f, str):
+            raise Exception("broken 'features' list in schema entry '%s'" % entry)
+
+
+# Validate that the passed schema has only supported members. This is useful to
+# stay up to date with any changes to the schema.
+def validate_schema(schemalist):
+    for entry in schemalist:
+        if not isinstance(entry, dict):
+            raise Exception("schema entry '%s' is not a JSON Object (dict)" % (entry))
+
+        match entry.get('meta-type', None):
+            case 'command':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('arg-type', str),
+                                      ('ret-type', str)],
+                           optional=[('features', list),
+                                     ('allow-oob', bool)])
+
+                check_features_list(entry)
+
+            case 'event':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('arg-type', str)],
+                           optional=[('features', list)])
+
+                check_features_list(entry)
+
+            case 'object':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('members', list)],
+                           optional=[('tag', str),
+                                     ('variants', list),
+                                     ('features', list)])
+
+                check_features_list(entry)
+
+                for m in entry.get('members', []):
+                    check_keys(m,
+                               mandatory=[('name', str),
+                                          ('type', str)],
+                               optional=[('default', None),
+                                         ('features', list)])
+                    check_features_list(m)
+
+                for m in entry.get('variants', []):
+                    check_keys(m,
+                               mandatory=[('case', str),
+                                          ('type', str)],
+                               optional=[])
+
+            case 'array':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('element-type', str)],
+                           optional=[])
+
+            case 'enum':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str)],
+                           optional=[('members', list),
+                                     ('values', list)])
+
+                for m in entry.get('members', []):
+                    check_keys(m,
+                               mandatory=[('name', str)],
+                               optional=[('features', list)])
+                    check_features_list(m)
+
+            case 'alternate':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('members', list)],
+                           optional=[])
+
+                for m in entry.get('members', []):
+                    check_keys(m,
+                               mandatory=[('type', str)],
+                               optional=[])
+            case 'builtin':
+                check_keys(entry,
+                           mandatory=[('name', str),
+                                      ('meta-type', str),
+                                      ('json-type', str)],
+                           optional=[])
+
+            case _:
+                raise Exception("unknown or missing 'meta-type' in schema entry '%s'" % entry)
+
+
+# Convert a list of QMP schema entries into a dict organized via 'name' member
+def load_schema_json_dict(schemalist):
+    schemadict = {}
+
+    for memb in schemalist:
+        schemadict[memb['name']] = memb
+
+    return schemadict
+
+
+# loads and validates the QMP schema from the .replies file 'filename'
+def load_schema(filename):
+    schemalist = load_schema_json_list(filename)
+
+    if not schemalist:
+        raise Exception("QMP schema not found in '%s'" % (filename))
+
+    validate_schema(schemalist)
+
+    return load_schema_json_dict(schemalist)
+
+
+# Recursively traverse the schema and print out the schema query strings for
+# the corresponding entries. In certain cases the schema references itself,
+# which is handled by passing a 'trace' list which contains the current path
+def iterate_schema(name, cur, trace, schema):
+    obj = schema[name]
+
+    if name in trace:
+        print('%s (recursion)' % cur)
+        return
+
+    trace = trace + [name]
+
+    match obj['meta-type']:
+        case 'command' | 'event':
+            arguments = obj.get('arg-type', None)
+            returns = obj.get('ret-type', None)
+
+            print(name)
+
+            for f in obj.get('features', []):
+                print('%s/$%s' % (cur, f))
+
+            if arguments:
+                iterate_schema(arguments, cur + '/arg-type', trace, schema)
+
+            if returns:
+                iterate_schema(returns, cur + '/ret-type', trace, schema)
+
+        case 'object':
+            members = sorted(obj.get('members', []), key=lambda d: d['name'])
+            variants = sorted(obj.get('variants', []), key=lambda d: d['case'])
+
+            for f in obj.get('features', []):
+                print('%s/$%s' % (cur, f))
+
+            for memb in members:
+                membpath = "%s/%s" % (cur, memb['name'])
+                print(membpath)
+
+                for f in memb.get('features', []):
+                    print('%s/$%s' % (membpath, f))
+
+                iterate_schema(memb['type'], membpath, trace, schema)
+
+            for var in variants:
+                varpath = "%s/+%s" % (cur, var['case'])
+                print(varpath)
+                iterate_schema(var['type'], varpath, trace, schema)
+
+        case 'enum':
+            members = sorted(obj.get('members', []), key=lambda d: d['name'])
+
+            for m in members:
+                print('%s/^%s' % (cur, m['name']))
+
+                for f in m.get('features', []):
+                    print('%s/^%s/$%s' % (cur, m['name'], f))
+
+        case 'array':
+            iterate_schema(obj['element-type'], cur, trace, schema)
+
+        case 'builtin':
+            print('%s/!%s' % (cur, name))
+
+        case 'alternate':
+            for var in obj['members']:
+                iterate_schema(var['type'], cur, trace, schema)
+
+        case _:
+            raise Exception("unhandled 'meta-type' '%s'" % obj.get('meta-type', '<missing>'))
+
+
+def process_one_schema(schemafile, validate_only):
+    try:
+        schema = load_schema(schemafile)
+    except Exception as e:
+        raise Exception("Failed to load schema '%s': %s" % (schemafile, e))
+
+    if validate_only:
+        return
+
+    toplevel = []
+
+    for k, v in schema.items():
+        if v['meta-type'] == 'command' or v['meta-type'] == 'event':
+            toplevel.append(k)
+
+    toplevel.sort()
+
+    for c in toplevel:
+        iterate_schema(c, c, [], schema)
+
+
+parser = argparse.ArgumentParser(description='A tool to generate QMP schema query strins and validator of schema coverage')
+parser.add_argument('--validate', action="store_true", help='only load the schema and validate it')
+parser.add_argument('--schemadir', default='',
+                    help='directory containing .replies files')
+parser.add_argument('schema', nargs='?', help='path to .replies file to use')
+args = parser.parse_args()
+
+if not args.schema and not args.schemadir:
+    parser.print_help()
+    sys.exit(1)
+
+if args.schema:
+    process_one_schema(args.schema, args.validate)
+else:
+    files = Path(args.schemadir).glob('*.replies')
+
+    for file in files:
+        process_one_schema(str(file), args.validate)
diff --git a/tests/meson.build b/tests/meson.build
index 0082446029..25e7ccd312 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -598,6 +598,18 @@ foreach data : tests
   test(data['name'], test_bin, env: tests_env, timeout: timeout, depends: tests_deps)
 endforeach

+test(
+  'qapi-schema-check',
+  python3_prog,
+  args: [
+    qapi_schema_diff_gen_prog.full_path(),
+    '--validate',
+    '--schemadir',
+    meson.project_source_root() / 'tests' / 'qemucapabilitiesdata'
+  ],
+  env: runutf8,
+)
+

 # helpers:
 #   each entry is a dictionary with following items:
-- 
2.40.1
Re: [PATCH] scripts: QMP schema query string helper script
Posted by Jonathon Jongsma 11 months, 2 weeks ago
On 6/7/23 10:08 AM, Peter Krempa wrote:
> The script generates all query strings in same format as used for
> querying qemu capabilities for a given .replies file.
> 
> The output can be used to either aid in creation of the query strings as
> well as to generate diff between the schema, which is useful when adding
> a new capability dump.
> 
> The script also validates that all of the schema is supported by our
> tools so that we can always adapt.
> 
> The output looks like:
> 
>   $ ./scripts/qapi-schema-diff-gen.py tests/qemucapabilitiesdata/caps_8.0.0_x86_64.replies

I feel like the script could use a better name. *-diff-gen implies that 
it is generating a diff between two things. But currently it's not 
generating any diffs. It's actually just generating a list of valid 
schema query strings from a .replies file.

My suggestion:
Name the script something like 'qapi-schema-validate.py' and change the 
behavior so it only validates by default, but add a -l/--list-queries 
option to printout a list of all valid query strings.

>   [...]
>   query-yank
>   query-yank/ret-type/type
>   query-yank/ret-type/type/^block-node
>   query-yank/ret-type/type/^chardev
>   query-yank/ret-type/type/^migration
>   query-yank/ret-type/+block-node
>   query-yank/ret-type/+block-node/node-name
>   query-yank/ret-type/+block-node/node-name/!str
>   query-yank/ret-type/+chardev
>   query-yank/ret-type/+chardev/id
>   query-yank/ret-type/+chardev/id/!str
>   query-yank/ret-type/+migration
>   [...]
> 
> Signed-off-by: Peter Krempa <pkrempa@redhat.com>
> ---
>   scripts/meson.build             |   1 +
>   scripts/qapi-schema-diff-gen.py | 313 ++++++++++++++++++++++++++++++++
>   tests/meson.build               |  12 ++
>   3 files changed, 326 insertions(+)
>   create mode 100755 scripts/qapi-schema-diff-gen.py
> 
> diff --git a/scripts/meson.build b/scripts/meson.build
> index 05b71184f1..c216c7e1ff 100644
> --- a/scripts/meson.build
> +++ b/scripts/meson.build
> @@ -30,6 +30,7 @@ scripts = [
>     'meson-timestamp.py',
>     'mock-noinline.py',
>     'prohibit-duplicate-header.py',
> +  'qapi-schema-diff-gen.py',
>   ]
> 
>   foreach name : scripts
> diff --git a/scripts/qapi-schema-diff-gen.py b/scripts/qapi-schema-diff-gen.py
> new file mode 100755
> index 0000000000..bde130e33c
> --- /dev/null
> +++ b/scripts/qapi-schema-diff-gen.py
> @@ -0,0 +1,313 @@
> +#!/usr/bin/env python3
> +#
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +#
> +# A tool to help with creating query strings for querying the QMP schema (as
> +# returned by 'query-qmp-schema' QMP command). In default mode it generates all
> +# the possible query strings for a QMP schema in the ".replies" format as
> +# generated by 'tests/qemucapsprobe.c'. This can be either used by users to
> +# find the desired schema query string or to see what changed between two
> +# versions.
> +#
> +# In the '--validate' mode the script doesn't output the schema query strings.
> +# This invokes just the validator that everything in the schema is supported by
> +# the tool.
> +#
> +# Note: Any change to the 'validate_schema' function below to make it accept
> +# new schema components most likely requires change to either
> +# 'src/qemu/qemu_qapi.c' or 'tests/testutilsqemuschema.c' to accept the new
> +# schema components.
> +
> +from pathlib import Path
> +import argparse
> +import json
> +import sys
> +
> +
> +# Finds the apropriate call to 'query-qmp-schema' in the '.replies' file and
> +# returns the JSON blob following the command invocation.
> +def load_schema_json_list(filename):
> +    found = False
> +
> +    with open(filename, "r") as fh:
> +        jsonstr = ''
> +        for line in fh:
> +            jsonstr += line
> +
> +            if line != '}\n':
> +                continue
> +
> +            if found:
> +                return json.loads(jsonstr)["return"]
> +
> +            cmdobj = json.loads(jsonstr)
> +            jsonstr = ""
> +
> +            if isinstance(cmdobj, dict) and cmdobj.get('execute', '') == 'query-qmp-schema':
> +                found = True
> +
> +
> +# Validates that 'entry' (an member of the QMP schema):

s/that //
s/an/a/

> +# - checks that it's a Dict (imported from a JSON object)
> +# - checks that all 'mandatory' fields are present and their types match
> +# - checks the types of all 'optional' fields
> +# - checks that no unknown fields are present
> +def check_keys(entry, mandatory, optional):
> +    keys = set(entry.keys())
> +
> +    for k, t in mandatory:
> +        try:
> +            keys.remove(k)
> +        except KeyError:
> +            raise Exception("missing mandatory key '%s' in schema '%s'" % (k, entry))
> +
> +        if not isinstance(entry[k], t):
> +            raise Exception("key '%s' is not of the expected type '%s' in schema '%s'" % (k, t, entry))
> +
> +    for k, t in optional:
> +        if k in keys:
> +            keys.discard(k)
> +
> +            if t is not None:
> +                if not isinstance(entry[k], t):
> +                    raise Exception("key '%s' is not of the expected type '%s' in schema '%s'" % (k, t, entry))
> +
> +    if len(keys) > 0:
> +        raise Exception("unhandled keys '%s' in schema '%s'" % (','.join(list(keys)), entry))
> +
> +
> +# Validates the optional 'features' and that they consist only of strings
> +def check_features_list(entry):
> +    for f in entry.get('features', []):
> +        if not isinstance(f, str):
> +            raise Exception("broken 'features' list in schema entry '%s'" % entry)
> +
> +
> +# Validate that the passed schema has only supported members. This is useful to
> +# stay up to date with any changes to the schema.
> +def validate_schema(schemalist):
> +    for entry in schemalist:
> +        if not isinstance(entry, dict):
> +            raise Exception("schema entry '%s' is not a JSON Object (dict)" % (entry))
> +
> +        match entry.get('meta-type', None):
> +            case 'command':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('arg-type', str),
> +                                      ('ret-type', str)],
> +                           optional=[('features', list),
> +                                     ('allow-oob', bool)])
> +
> +                check_features_list(entry)
> +
> +            case 'event':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('arg-type', str)],
> +                           optional=[('features', list)])
> +
> +                check_features_list(entry)
> +
> +            case 'object':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('members', list)],
> +                           optional=[('tag', str),
> +                                     ('variants', list),
> +                                     ('features', list)])
> +
> +                check_features_list(entry)
> +
> +                for m in entry.get('members', []):
> +                    check_keys(m,
> +                               mandatory=[('name', str),
> +                                          ('type', str)],
> +                               optional=[('default', None),
> +                                         ('features', list)])
> +                    check_features_list(m)
> +
> +                for m in entry.get('variants', []):
> +                    check_keys(m,
> +                               mandatory=[('case', str),
> +                                          ('type', str)],
> +                               optional=[])
> +
> +            case 'array':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('element-type', str)],
> +                           optional=[])
> +
> +            case 'enum':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str)],
> +                           optional=[('members', list),
> +                                     ('values', list)])
> +
> +                for m in entry.get('members', []):
> +                    check_keys(m,
> +                               mandatory=[('name', str)],
> +                               optional=[('features', list)])
> +                    check_features_list(m)
> +
> +            case 'alternate':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('members', list)],
> +                           optional=[])
> +
> +                for m in entry.get('members', []):
> +                    check_keys(m,
> +                               mandatory=[('type', str)],
> +                               optional=[])
> +            case 'builtin':
> +                check_keys(entry,
> +                           mandatory=[('name', str),
> +                                      ('meta-type', str),
> +                                      ('json-type', str)],
> +                           optional=[])
> +
> +            case _:
> +                raise Exception("unknown or missing 'meta-type' in schema entry '%s'" % entry)
> +
> +
> +# Convert a list of QMP schema entries into a dict organized via 'name' member
> +def load_schema_json_dict(schemalist):
> +    schemadict = {}
> +
> +    for memb in schemalist:
> +        schemadict[memb['name']] = memb
> +
> +    return schemadict
> +
> +
> +# loads and validates the QMP schema from the .replies file 'filename'
> +def load_schema(filename):
> +    schemalist = load_schema_json_list(filename)
> +
> +    if not schemalist:
> +        raise Exception("QMP schema not found in '%s'" % (filename))
> +
> +    validate_schema(schemalist)
> +
> +    return load_schema_json_dict(schemalist)
> +
> +
> +# Recursively traverse the schema and print out the schema query strings for
> +# the corresponding entries. In certain cases the schema references itself,
> +# which is handled by passing a 'trace' list which contains the current path
> +def iterate_schema(name, cur, trace, schema):
> +    obj = schema[name]
> +
> +    if name in trace:
> +        print('%s (recursion)' % cur)
> +        return
> +
> +    trace = trace + [name]
> +
> +    match obj['meta-type']:
> +        case 'command' | 'event':
> +            arguments = obj.get('arg-type', None)
> +            returns = obj.get('ret-type', None)
> +
> +            print(name)
> +
> +            for f in obj.get('features', []):
> +                print('%s/$%s' % (cur, f))
> +
> +            if arguments:
> +                iterate_schema(arguments, cur + '/arg-type', trace, schema)
> +
> +            if returns:
> +                iterate_schema(returns, cur + '/ret-type', trace, schema)
> +
> +        case 'object':
> +            members = sorted(obj.get('members', []), key=lambda d: d['name'])
> +            variants = sorted(obj.get('variants', []), key=lambda d: d['case'])
> +
> +            for f in obj.get('features', []):
> +                print('%s/$%s' % (cur, f))
> +
> +            for memb in members:
> +                membpath = "%s/%s" % (cur, memb['name'])
> +                print(membpath)
> +
> +                for f in memb.get('features', []):
> +                    print('%s/$%s' % (membpath, f))
> +
> +                iterate_schema(memb['type'], membpath, trace, schema)
> +
> +            for var in variants:
> +                varpath = "%s/+%s" % (cur, var['case'])
> +                print(varpath)
> +                iterate_schema(var['type'], varpath, trace, schema)
> +
> +        case 'enum':
> +            members = sorted(obj.get('members', []), key=lambda d: d['name'])
> +
> +            for m in members:
> +                print('%s/^%s' % (cur, m['name']))
> +
> +                for f in m.get('features', []):
> +                    print('%s/^%s/$%s' % (cur, m['name'], f))
> +
> +        case 'array':
> +            iterate_schema(obj['element-type'], cur, trace, schema)
> +
> +        case 'builtin':
> +            print('%s/!%s' % (cur, name))
> +
> +        case 'alternate':
> +            for var in obj['members']:
> +                iterate_schema(var['type'], cur, trace, schema)
> +
> +        case _:
> +            raise Exception("unhandled 'meta-type' '%s'" % obj.get('meta-type', '<missing>'))
> +
> +
> +def process_one_schema(schemafile, validate_only):
> +    try:
> +        schema = load_schema(schemafile)
> +    except Exception as e:
> +        raise Exception("Failed to load schema '%s': %s" % (schemafile, e))
> +
> +    if validate_only:
> +        return
> +
> +    toplevel = []
> +
> +    for k, v in schema.items():
> +        if v['meta-type'] == 'command' or v['meta-type'] == 'event':
> +            toplevel.append(k)
> +
> +    toplevel.sort()
> +
> +    for c in toplevel:
> +        iterate_schema(c, c, [], schema)
> +
> +
> +parser = argparse.ArgumentParser(description='A tool to generate QMP schema query strins and validator of schema coverage')

typo: strins -> strings

What do you mean by 'coverage' here? How is it validating schema 
coverage? As far as I can tell, it's just validating the schema replies 
file itself.

I would change this help text to something like "A tool to validate QMP 
schema .replies files and generate a list of valid schema query strings"

Reviewed-by: Jonathon Jongsma <jjongsma@redhat.com>


> +parser.add_argument('--validate', action="store_true", help='only load the schema and validate it')
> +parser.add_argument('--schemadir', default='',
> +                    help='directory containing .replies files')
> +parser.add_argument('schema', nargs='?', help='path to .replies file to use')
> +args = parser.parse_args()
> +
> +if not args.schema and not args.schemadir:
> +    parser.print_help()
> +    sys.exit(1)
> +
> +if args.schema:
> +    process_one_schema(args.schema, args.validate)
> +else:
> +    files = Path(args.schemadir).glob('*.replies')
> +
> +    for file in files:
> +        process_one_schema(str(file), args.validate)
> diff --git a/tests/meson.build b/tests/meson.build
> index 0082446029..25e7ccd312 100644
> --- a/tests/meson.build
> +++ b/tests/meson.build
> @@ -598,6 +598,18 @@ foreach data : tests
>     test(data['name'], test_bin, env: tests_env, timeout: timeout, depends: tests_deps)
>   endforeach
> 
> +test(
> +  'qapi-schema-check',
> +  python3_prog,
> +  args: [
> +    qapi_schema_diff_gen_prog.full_path(),
> +    '--validate',
> +    '--schemadir',
> +    meson.project_source_root() / 'tests' / 'qemucapabilitiesdata'
> +  ],
> +  env: runutf8,
> +)
> +
> 
>   # helpers:
>   #   each entry is a dictionary with following items: