All of lore.kernel.org
 help / color / mirror / Atom feed
From: Victor Toso <victortoso@redhat.com>
To: qemu-devel@nongnu.org
Cc: Eric Blake <eblake@redhat.com>,
	Markus Armbruster <armbru@redhat.com>,
	John Snow <jsnow@redhat.com>, Michael Roth <michael.roth@amd.com>
Subject: [PATCH v1 16/16] RFC: add a generator for qapi's examples
Date: Tue, 30 Aug 2022 18:15:45 +0200	[thread overview]
Message-ID: <20220830161545.84198-17-victortoso@redhat.com> (raw)
In-Reply-To: <20220830161545.84198-1-victortoso@redhat.com>

The goal of this generator is to validate QAPI examples and transform
them into a format that can be used for 3rd party applications to
validate their QAPI/QMP introspection.

For each Example section, we parse server and client messages into a
python dictionary. This step alone has found several ill formatted
JSON messages in the examples.

The generator outputs another JSON file with all the examples in the
QAPI module that they came from. This can be used to validate the
introspection between QAPI/QMP to language bindings.

When used with the POC qapi-go branch, we have found bad QMP messages
with wrong member names, mandatory members that were missing and
optional members that were being set with null (not needed).

A simple example of the output format is:

 { "examples": [
   {
     "id": "ksuxwzfayw",
     "client": [
     {
       "sequence-order": 1
       "message-type": "command",
       "message":
       { "arguments":
         { "device": "scratch", "size": 1073741824 },
         "execute": "block_resize"
       },
    } ],
    "server": [
    {
      "sequence-order": 2
      "message-type": "return",
      "message": { "return": {} },
    } ]
    }
  ] }

If this idea seems reasonable, we can add python-qemu-qmp to validate
each message at generation time already.

Signed-off-by: Victor Toso <victortoso@redhat.com>
---
 scripts/qapi/dumpexamples.py | 194 +++++++++++++++++++++++++++++++++++
 scripts/qapi/main.py         |   2 +
 2 files changed, 196 insertions(+)
 create mode 100644 scripts/qapi/dumpexamples.py

diff --git a/scripts/qapi/dumpexamples.py b/scripts/qapi/dumpexamples.py
new file mode 100644
index 0000000000..c14ed11774
--- /dev/null
+++ b/scripts/qapi/dumpexamples.py
@@ -0,0 +1,194 @@
+"""
+Dump examples for Developers
+"""
+# Copyright (c) 2022 Red Hat Inc.
+#
+# Authors:
+#  Victor Toso <victortoso@redhat.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+
+# Just for type hint on self
+from __future__ import annotations
+
+import os
+import json
+import random
+import string
+
+from typing import Dict, List, Optional
+
+from .schema import (
+    QAPISchema,
+    QAPISchemaType,
+    QAPISchemaVisitor,
+    QAPISchemaEnumMember,
+    QAPISchemaFeature,
+    QAPISchemaIfCond,
+    QAPISchemaObjectType,
+    QAPISchemaObjectTypeMember,
+    QAPISchemaVariants,
+)
+from .source import QAPISourceInfo
+
+
+def gen_examples(schema: QAPISchema,
+                 output_dir: str,
+                 prefix: str) -> None:
+    vis = QAPISchemaGenExamplesVisitor(prefix)
+    schema.visit(vis)
+    vis.write(output_dir)
+
+
+def get_id(random, size: int) -> str:
+    letters = string.ascii_lowercase
+    return ''.join(random.choice(letters) for i in range(size))
+
+
+def next_object(text, start, end, context) -> Dict:
+    # Start of json object
+    start = text.find("{", start)
+    end = text.rfind("}", start, end+1)
+
+    # try catch, pretty print issues
+    try:
+        ret = json.loads(text[start:end+1])
+    except Exception as e:
+        print("Error: {}\nLocation: {}\nData: {}\n".format(
+              str(e), context, text[start:end+1]))
+        return {}
+    else:
+        return ret
+
+
+def parse_text_to_dicts(text: str, context: str) -> List[Dict]:
+    examples, clients, servers = [], [], []
+
+    count = 1
+    c, s = text.find("->"), text.find("<-")
+    while c != -1 or s != -1:
+        if c == -1 or (s != -1 and s < c):
+            start, target = s, servers
+        else:
+            start, target = c, clients
+
+        # Find the client and server, if any
+        if c != -1:
+            c = text.find("->", start + 1)
+        if s != -1:
+            s = text.find("<-", start + 1)
+
+        # Find the limit of current's object.
+        # We first look for the next message, either client or server. If none
+        # is avaible, we set the end of the text as limit.
+        if c == -1 and s != -1:
+            end = s
+        elif c != -1 and s == -1:
+            end = c
+        elif c != -1 and s != -1:
+            end = (c < s) and c or s
+        else:
+            end = len(text) - 1
+
+        message = next_object(text, start, end, context)
+        if len(message) > 0:
+            message_type = "return"
+            if "execute" in message:
+                message_type = "command"
+            elif "event" in message:
+                message_type = "event"
+
+            target.append({
+                "sequence-order": count,
+                "message-type": message_type,
+                "message": message
+            })
+            count += 1
+
+    examples.append({"client": clients, "server": servers})
+    return examples
+
+
+def parse_examples_of(self: QAPISchemaGenExamplesVisitor,
+                      name: str):
+
+    assert(name in self.schema._entity_dict)
+    obj = self.schema._entity_dict[name]
+    assert((obj.doc is not None))
+    module_name = obj._module.name
+
+    # We initialize random with the name so that we get consistent example
+    # ids over different generations. The ids of a given example might
+    # change when adding/removing examples, but that's acceptable as the
+    # goal is just to grep $id to find what example failed at a given test
+    # with minimum chorn over regenerating.
+    random.seed(name, version=2)
+
+    for s in obj.doc.sections:
+        if s.name != "Example":
+            continue
+
+        if module_name not in self.target:
+            self.target[module_name] = []
+
+        context = f'''{name} at {obj.info.fname}:{obj.info.line}'''
+        examples = parse_text_to_dicts(s.text, context)
+        for example in examples:
+            self.target[module_name].append({
+                    "id": get_id(random, 10),
+                    "client": example["client"],
+                    "server": example["server"]
+            })
+
+
+class QAPISchemaGenExamplesVisitor(QAPISchemaVisitor):
+
+    def __init__(self, prefix: str):
+        super().__init__()
+        self.target = {}
+        self.schema = None
+
+    def visit_begin(self, schema):
+        self.schema = schema
+
+    def visit_end(self):
+        self.schema = None
+
+    def write(self: QAPISchemaGenExamplesVisitor,
+              output_dir: str) -> None:
+        for filename, content in self.target.items():
+            pathname = os.path.join(output_dir, "examples", filename)
+            odir = os.path.dirname(pathname)
+            os.makedirs(odir, exist_ok=True)
+            result = {"examples": content}
+
+            with open(pathname, "w") as outfile:
+                outfile.write(json.dumps(result, indent=2, sort_keys=True))
+
+    def visit_command(self: QAPISchemaGenExamplesVisitor,
+                      name: str,
+                      info: Optional[QAPISourceInfo],
+                      ifcond: QAPISchemaIfCond,
+                      features: List[QAPISchemaFeature],
+                      arg_type: Optional[QAPISchemaObjectType],
+                      ret_type: Optional[QAPISchemaType],
+                      gen: bool,
+                      success_response: bool,
+                      boxed: bool,
+                      allow_oob: bool,
+                      allow_preconfig: bool,
+                      coroutine: bool) -> None:
+
+        if gen:
+            parse_examples_of(self, name)
+
+    def visit_event(self: QAPISchemaGenExamplesVisitor,
+                    name: str,
+                    info: Optional[QAPISourceInfo],
+                    ifcond: QAPISchemaIfCond,
+                    features: List[QAPISchemaFeature],
+                    arg_type: Optional[QAPISchemaObjectType],
+                    boxed: bool):
+
+        parse_examples_of(self, name)
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index fc216a53d3..9e771f4dd3 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -13,6 +13,7 @@
 
 from .commands import gen_commands
 from .common import must_match
+from .dumpexamples import gen_examples
 from .error import QAPIError
 from .events import gen_events
 from .introspect import gen_introspect
@@ -54,6 +55,7 @@ def generate(schema_file: str,
     gen_events(schema, output_dir, prefix)
     gen_introspect(schema, output_dir, prefix, unmask)
 
+    gen_examples(schema, output_dir, prefix)
 
 def main() -> int:
     """
-- 
2.37.2



  parent reply	other threads:[~2022-08-30 16:40 UTC|newest]

Thread overview: 36+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-08-30 16:15 [PATCH v1 00/16] qapi examples fixes and rfc for another generator Victor Toso
2022-08-30 16:15 ` [PATCH v1 01/16] qapi: fix example of query-ballon command Victor Toso
2022-08-30 16:15 ` [PATCH v1 02/16] qapi: fix example of query-vnc command Victor Toso
2022-08-30 16:15 ` [PATCH v1 03/16] qapi: fix example of query-spice command Victor Toso
2022-08-31 11:50   ` Markus Armbruster
2022-08-31 12:55     ` Victor Toso
2022-08-31 13:22       ` Markus Armbruster
2022-09-01 14:08         ` Gerd Hoffmann
2022-08-30 16:15 ` [PATCH v1 04/16] qapi: fix example of query-rocker-of-dpa-flows command Victor Toso
2022-08-31 11:51   ` Markus Armbruster
2022-08-30 16:15 ` [PATCH v1 05/16] qapi: fix example of query-dump-guest-memory-capability command Victor Toso
2022-08-30 16:15 ` [PATCH v1 06/16] qapi: fix example of query-blockstats command Victor Toso
2022-08-30 16:15 ` [PATCH v1 07/16] qapi: fix example of BLOCK_JOB_READY event Victor Toso
2022-08-30 16:15 ` [PATCH v1 08/16] qapi: fix example of NIC_RX_FILTER_CHANGED event Victor Toso
2022-08-31 11:37   ` Markus Armbruster
2022-08-30 16:15 ` [PATCH v1 09/16] qapi: fix example of DEVICE_UNPLUG_GUEST_ERROR event Victor Toso
2022-08-30 16:15 ` [PATCH v1 10/16] qapi: fix example of MEM_UNPLUG_ERROR event Victor Toso
2022-08-30 16:15 ` [PATCH v1 11/16] qapi: fix examples of blockdev-add with qcow2 Victor Toso
2022-08-30 16:15 ` [PATCH v1 12/16] qapi: fix example of blockdev-add command Victor Toso
2022-08-31 11:40   ` Markus Armbruster
2022-08-31 12:45     ` Victor Toso
2022-08-31 13:16       ` Markus Armbruster
2022-08-31 13:47         ` Victor Toso
2022-08-31 14:53           ` Markus Armbruster
2022-09-01  7:56             ` Victor Toso
2022-09-01 11:13               ` Markus Armbruster
2022-09-02  8:02                 ` Victor Toso
2022-08-30 16:15 ` [PATCH v1 13/16] qapi: fix example of query-hotpluggable-cpus command Victor Toso
2022-08-30 16:15 ` [PATCH v1 14/16] qapi: fix example of query-migrate command Victor Toso
2022-08-31 11:52   ` Markus Armbruster
2022-08-30 16:15 ` [PATCH v1 15/16] qapi: fix examples of events missing timestamp Victor Toso
2022-08-30 16:15 ` Victor Toso [this message]
2022-08-31 12:01   ` [PATCH v1 16/16] RFC: add a generator for qapi's examples Markus Armbruster
2022-08-31 13:32     ` Victor Toso
2022-08-31 14:57       ` Markus Armbruster
2022-09-01  8:37         ` Victor Toso

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220830161545.84198-17-victortoso@redhat.com \
    --to=victortoso@redhat.com \
    --cc=armbru@redhat.com \
    --cc=eblake@redhat.com \
    --cc=jsnow@redhat.com \
    --cc=michael.roth@amd.com \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.