qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Qemu-devel] [PATCH v5 13/17] qapi: add qapi2texi script


From: Marc-André Lureau
Subject: [Qemu-devel] [PATCH v5 13/17] qapi: add qapi2texi script
Date: Thu, 17 Nov 2016 19:55:00 +0400

As the name suggests, the qapi2texi script converts JSON QAPI
description into a texi file suitable for different target
formats (info/man/txt/pdf/html...).

It parses the following kind of blocks:

Free-form:

  ##
  # = Section
  # == Subsection
  #
  # Some text foo with *emphasis*
  # 1. with a list
  # 2. like that
  #
  # And some code:
  # | $ echo foo
  # | -> do this
  # | <- get that
  #
  ##

Symbol:

  ##
  # @symbol:
  #
  # Symbol body ditto ergo sum. Foo bar
  # baz ding.
  #
  # @arg: foo
  # @arg: #optional foo
  #
  # Returns: returns bla bla
  #          Or bla blah
  #
  # Since: version
  # Notes: notes, comments can have
  #        - itemized list
  #        - like this
  #
  # Example:
  #
  # -> { "execute": "quit" }
  # <- { "return": {} }
  #
  ##

That's roughly following the following EBNF grammar:

api_comment = "##\n" comment "##\n"
comment = freeform_comment | symbol_comment
freeform_comment = { "# " text "\n" | "#\n" }
symbol_comment = "# @" name ":\n" { member | meta | freeform_comment }
member = "# @" name ':' [ text ] freeform_comment
meta = "# " ( "Returns:", "Since:", "Note:", "Notes:", "Example:", "Examples:" 
) [ text ] freeform_comment
text = free-text markdown-like, "#optional" for members

Thanks to the following json expressions, the documentation is enhanced
with extra information about the type of arguments and return value
expected.

Signed-off-by: Marc-André Lureau <address@hidden>
---
 tests/Makefile.include                       |  17 ++
 scripts/qapi.py                              | 215 ++++++++++++++++-
 scripts/qapi2texi.py                         | 331 +++++++++++++++++++++++++++
 docs/qapi-code-gen.txt                       |  54 ++++-
 tests/qapi-schema/doc-bad-args.err           |   1 +
 tests/qapi-schema/doc-bad-args.exit          |   1 +
 tests/qapi-schema/doc-bad-args.json          |   6 +
 tests/qapi-schema/doc-bad-args.out           |   0
 tests/qapi-schema/doc-bad-symbol.err         |   1 +
 tests/qapi-schema/doc-bad-symbol.exit        |   1 +
 tests/qapi-schema/doc-bad-symbol.json        |   4 +
 tests/qapi-schema/doc-bad-symbol.out         |   0
 tests/qapi-schema/doc-duplicated-arg.err     |   1 +
 tests/qapi-schema/doc-duplicated-arg.exit    |   1 +
 tests/qapi-schema/doc-duplicated-arg.json    |   5 +
 tests/qapi-schema/doc-duplicated-arg.out     |   0
 tests/qapi-schema/doc-duplicated-return.err  |   1 +
 tests/qapi-schema/doc-duplicated-return.exit |   1 +
 tests/qapi-schema/doc-duplicated-return.json |   6 +
 tests/qapi-schema/doc-duplicated-return.out  |   0
 tests/qapi-schema/doc-duplicated-since.err   |   1 +
 tests/qapi-schema/doc-duplicated-since.exit  |   1 +
 tests/qapi-schema/doc-duplicated-since.json  |   6 +
 tests/qapi-schema/doc-duplicated-since.out   |   0
 tests/qapi-schema/doc-empty-arg.err          |   1 +
 tests/qapi-schema/doc-empty-arg.exit         |   1 +
 tests/qapi-schema/doc-empty-arg.json         |   4 +
 tests/qapi-schema/doc-empty-arg.out          |   0
 tests/qapi-schema/doc-empty-section.err      |   1 +
 tests/qapi-schema/doc-empty-section.exit     |   1 +
 tests/qapi-schema/doc-empty-section.json     |   6 +
 tests/qapi-schema/doc-empty-section.out      |   0
 tests/qapi-schema/doc-empty-symbol.err       |   1 +
 tests/qapi-schema/doc-empty-symbol.exit      |   1 +
 tests/qapi-schema/doc-empty-symbol.json      |   3 +
 tests/qapi-schema/doc-empty-symbol.out       |   0
 tests/qapi-schema/doc-invalid-end.err        |   1 +
 tests/qapi-schema/doc-invalid-end.exit       |   1 +
 tests/qapi-schema/doc-invalid-end.json       |   3 +
 tests/qapi-schema/doc-invalid-end.out        |   0
 tests/qapi-schema/doc-invalid-end2.err       |   1 +
 tests/qapi-schema/doc-invalid-end2.exit      |   1 +
 tests/qapi-schema/doc-invalid-end2.json      |   3 +
 tests/qapi-schema/doc-invalid-end2.out       |   0
 tests/qapi-schema/doc-invalid-return.err     |   1 +
 tests/qapi-schema/doc-invalid-return.exit    |   1 +
 tests/qapi-schema/doc-invalid-return.json    |   5 +
 tests/qapi-schema/doc-invalid-return.out     |   0
 tests/qapi-schema/doc-invalid-section.err    |   1 +
 tests/qapi-schema/doc-invalid-section.exit   |   1 +
 tests/qapi-schema/doc-invalid-section.json   |   4 +
 tests/qapi-schema/doc-invalid-section.out    |   0
 tests/qapi-schema/doc-invalid-start.err      |   1 +
 tests/qapi-schema/doc-invalid-start.exit     |   1 +
 tests/qapi-schema/doc-invalid-start.json     |   3 +
 tests/qapi-schema/doc-invalid-start.out      |   0
 tests/qapi-schema/doc-missing-expr.err       |   1 +
 tests/qapi-schema/doc-missing-expr.exit      |   1 +
 tests/qapi-schema/doc-missing-expr.json      |   3 +
 tests/qapi-schema/doc-missing-expr.out       |   0
 tests/qapi-schema/doc-missing-space.err      |   1 +
 tests/qapi-schema/doc-missing-space.exit     |   1 +
 tests/qapi-schema/doc-missing-space.json     |   4 +
 tests/qapi-schema/doc-missing-space.out      |   0
 tests/qapi-schema/qapi-schema-test.json      |  58 +++++
 tests/qapi-schema/qapi-schema-test.out       |  49 ++++
 tests/qapi-schema/test-qapi.py               |  12 +
 67 files changed, 817 insertions(+), 14 deletions(-)
 create mode 100755 scripts/qapi2texi.py
 create mode 100644 tests/qapi-schema/doc-bad-args.err
 create mode 100644 tests/qapi-schema/doc-bad-args.exit
 create mode 100644 tests/qapi-schema/doc-bad-args.json
 create mode 100644 tests/qapi-schema/doc-bad-args.out
 create mode 100644 tests/qapi-schema/doc-bad-symbol.err
 create mode 100644 tests/qapi-schema/doc-bad-symbol.exit
 create mode 100644 tests/qapi-schema/doc-bad-symbol.json
 create mode 100644 tests/qapi-schema/doc-bad-symbol.out
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.err
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.json
 create mode 100644 tests/qapi-schema/doc-duplicated-arg.out
 create mode 100644 tests/qapi-schema/doc-duplicated-return.err
 create mode 100644 tests/qapi-schema/doc-duplicated-return.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-return.json
 create mode 100644 tests/qapi-schema/doc-duplicated-return.out
 create mode 100644 tests/qapi-schema/doc-duplicated-since.err
 create mode 100644 tests/qapi-schema/doc-duplicated-since.exit
 create mode 100644 tests/qapi-schema/doc-duplicated-since.json
 create mode 100644 tests/qapi-schema/doc-duplicated-since.out
 create mode 100644 tests/qapi-schema/doc-empty-arg.err
 create mode 100644 tests/qapi-schema/doc-empty-arg.exit
 create mode 100644 tests/qapi-schema/doc-empty-arg.json
 create mode 100644 tests/qapi-schema/doc-empty-arg.out
 create mode 100644 tests/qapi-schema/doc-empty-section.err
 create mode 100644 tests/qapi-schema/doc-empty-section.exit
 create mode 100644 tests/qapi-schema/doc-empty-section.json
 create mode 100644 tests/qapi-schema/doc-empty-section.out
 create mode 100644 tests/qapi-schema/doc-empty-symbol.err
 create mode 100644 tests/qapi-schema/doc-empty-symbol.exit
 create mode 100644 tests/qapi-schema/doc-empty-symbol.json
 create mode 100644 tests/qapi-schema/doc-empty-symbol.out
 create mode 100644 tests/qapi-schema/doc-invalid-end.err
 create mode 100644 tests/qapi-schema/doc-invalid-end.exit
 create mode 100644 tests/qapi-schema/doc-invalid-end.json
 create mode 100644 tests/qapi-schema/doc-invalid-end.out
 create mode 100644 tests/qapi-schema/doc-invalid-end2.err
 create mode 100644 tests/qapi-schema/doc-invalid-end2.exit
 create mode 100644 tests/qapi-schema/doc-invalid-end2.json
 create mode 100644 tests/qapi-schema/doc-invalid-end2.out
 create mode 100644 tests/qapi-schema/doc-invalid-return.err
 create mode 100644 tests/qapi-schema/doc-invalid-return.exit
 create mode 100644 tests/qapi-schema/doc-invalid-return.json
 create mode 100644 tests/qapi-schema/doc-invalid-return.out
 create mode 100644 tests/qapi-schema/doc-invalid-section.err
 create mode 100644 tests/qapi-schema/doc-invalid-section.exit
 create mode 100644 tests/qapi-schema/doc-invalid-section.json
 create mode 100644 tests/qapi-schema/doc-invalid-section.out
 create mode 100644 tests/qapi-schema/doc-invalid-start.err
 create mode 100644 tests/qapi-schema/doc-invalid-start.exit
 create mode 100644 tests/qapi-schema/doc-invalid-start.json
 create mode 100644 tests/qapi-schema/doc-invalid-start.out
 create mode 100644 tests/qapi-schema/doc-missing-expr.err
 create mode 100644 tests/qapi-schema/doc-missing-expr.exit
 create mode 100644 tests/qapi-schema/doc-missing-expr.json
 create mode 100644 tests/qapi-schema/doc-missing-expr.out
 create mode 100644 tests/qapi-schema/doc-missing-space.err
 create mode 100644 tests/qapi-schema/doc-missing-space.exit
 create mode 100644 tests/qapi-schema/doc-missing-space.json
 create mode 100644 tests/qapi-schema/doc-missing-space.out

diff --git a/tests/Makefile.include b/tests/Makefile.include
index e98d3b6..f16764c 100644
--- a/tests/Makefile.include
+++ b/tests/Makefile.include
@@ -350,6 +350,21 @@ qapi-schema += base-cycle-direct.json
 qapi-schema += base-cycle-indirect.json
 qapi-schema += command-int.json
 qapi-schema += comments.json
+qapi-schema += doc-bad-args.json
+qapi-schema += doc-bad-symbol.json
+qapi-schema += doc-duplicated-arg.json
+qapi-schema += doc-duplicated-return.json
+qapi-schema += doc-duplicated-since.json
+qapi-schema += doc-empty-arg.json
+qapi-schema += doc-empty-section.json
+qapi-schema += doc-empty-symbol.json
+qapi-schema += doc-invalid-end.json
+qapi-schema += doc-invalid-end2.json
+qapi-schema += doc-invalid-return.json
+qapi-schema += doc-invalid-section.json
+qapi-schema += doc-invalid-start.json
+qapi-schema += doc-missing-expr.json
+qapi-schema += doc-missing-space.json
 qapi-schema += double-data.json
 qapi-schema += double-type.json
 qapi-schema += duplicate-key.json
@@ -443,6 +458,8 @@ qapi-schema += union-optional-branch.json
 qapi-schema += union-unknown.json
 qapi-schema += unknown-escape.json
 qapi-schema += unknown-expr-key.json
+
+
 check-qapi-schema-y := $(addprefix tests/qapi-schema/, $(qapi-schema))
 
 GENERATED_HEADERS += tests/test-qapi-types.h tests/test-qapi-visit.h \
diff --git a/scripts/qapi.py b/scripts/qapi.py
index 4d1b0e4..1b456b4 100644
--- a/scripts/qapi.py
+++ b/scripts/qapi.py
@@ -122,6 +122,109 @@ class QAPILineError(Exception):
             "%s:%d: %s" % (self.info['file'], self.info['line'], self.msg)
 
 
+class QAPIDoc(object):
+    class Section(object):
+        def __init__(self, name=""):
+            # optional section name (argument/member or section name)
+            self.name = name
+            # the list of strings for this section
+            self.content = []
+
+        def append(self, line):
+            self.content.append(line)
+
+        def __repr__(self):
+            return "\n".join(self.content).strip()
+
+    class ArgSection(Section):
+        pass
+
+    def __init__(self, parser):
+        self.parser = parser
+        self.symbol = None
+        self.body = QAPIDoc.Section()
+        # a dict {'arg': ArgSection, ...}
+        self.args = OrderedDict()
+        # a list of Section
+        self.meta = []
+        # the current section
+        self.section = self.body
+        # associated expression and info (set by expression parser)
+        self.expr = None
+        self.info = None
+
+    def has_meta(self, name):
+        """Returns True if the doc has a meta section 'name'"""
+        for i in self.meta:
+            if i.name == name:
+                return True
+        return False
+
+    def append(self, line):
+        """Adds a # comment line, to be parsed and added to current section"""
+        line = line[1:]
+        if not line:
+            self._append_freeform(line)
+            return
+
+        if line[0] != ' ':
+            raise QAPISchemaError(self.parser, "missing space after #")
+        line = line[1:]
+
+        if self.symbol:
+            self._append_symbol_line(line)
+        elif (not self.body.content and
+              line.startswith("@") and line.endswith(":")):
+            self.symbol = line[1:-1]
+            if not self.symbol:
+                raise QAPISchemaError(self.parser, "Invalid symbol")
+        else:
+            self._append_freeform(line)
+
+    def _append_symbol_line(self, line):
+        name = line.split(' ', 1)[0]
+
+        if name.startswith("@") and name.endswith(":"):
+            line = line[len(name)+1:]
+            self._start_args_section(name[1:-1])
+        elif name in ("Returns:", "Since:",
+                      # those are often singular or plural
+                      "Note:", "Notes:",
+                      "Example:", "Examples:"):
+            line = line[len(name)+1:]
+            self._start_meta_section(name[:-1])
+
+        self._append_freeform(line)
+
+    def _start_args_section(self, name):
+        if not name:
+            raise QAPISchemaError(self.parser, "Invalid argument name")
+        if name in self.args:
+            raise QAPISchemaError(self.parser, "'%s' arg duplicated" % name)
+        self.section = QAPIDoc.ArgSection(name)
+        self.args[name] = self.section
+
+    def _start_meta_section(self, name):
+        if name in ("Returns", "Since") and self.has_meta(name):
+            raise QAPISchemaError(self.parser,
+                                  "Duplicated '%s' section" % name)
+        self.section = QAPIDoc.Section(name)
+        self.meta.append(self.section)
+
+    def _append_freeform(self, line):
+        in_arg = isinstance(self.section, QAPIDoc.ArgSection)
+        if in_arg and self.section.content and not self.section.content[-1] \
+           and line and not line[0].isspace():
+            # an empty line followed by a non-indented
+            # comment ends the argument section
+            self.section = self.body
+            self._append_freeform(line)
+            return
+        if in_arg or not self.section.name.startswith("Example"):
+            line = line.strip()
+        self.section.append(line)
+
+
 class QAPISchemaParser(object):
 
     def __init__(self, fp, previously_included=[], incl_info=None):
@@ -137,11 +240,18 @@ class QAPISchemaParser(object):
         self.line = 1
         self.line_pos = 0
         self.exprs = []
+        self.docs = []
         self.accept()
 
         while self.tok is not None:
             info = {'file': fname, 'line': self.line,
                     'parent': self.incl_info}
+            if self.tok == '#' and self.val.startswith('##'):
+                doc = self.get_doc()
+                doc.info = info
+                self.docs.append(doc)
+                continue
+
             expr = self.get_expr(False)
             if isinstance(expr, dict) and "include" in expr:
                 if len(expr) != 1:
@@ -160,6 +270,7 @@ class QAPISchemaParser(object):
                         raise QAPILineError(info, "Inclusion loop for %s"
                                             % include)
                     inf = inf['parent']
+
                 # skip multiple include of the same file
                 if incl_abs_fname in previously_included:
                     continue
@@ -171,12 +282,38 @@ class QAPISchemaParser(object):
                 exprs_include = QAPISchemaParser(fobj, previously_included,
                                                  info)
                 self.exprs.extend(exprs_include.exprs)
+                self.docs.extend(exprs_include.docs)
             else:
                 expr_elem = {'expr': expr,
                              'info': info}
+                if self.docs and not self.docs[-1].expr:
+                    self.docs[-1].expr = expr
+                    expr_elem['doc'] = self.docs[-1]
+
                 self.exprs.append(expr_elem)
 
-    def accept(self):
+    def get_doc(self):
+        if self.val != '##':
+            raise QAPISchemaError(self, "Junk after '##' at start of "
+                                  "documentation comment")
+
+        doc = QAPIDoc(self)
+        self.accept(False)
+        while self.tok == '#':
+            if self.val.startswith('##'):
+                # End of doc comment
+                if self.val != '##':
+                    raise QAPISchemaError(self, "Junk after '##' at end of "
+                                          "documentation comment")
+                self.accept()
+                return doc
+            else:
+                doc.append(self.val)
+            self.accept(False)
+
+        raise QAPISchemaError(self, "Documentation comment must end with '##'")
+
+    def accept(self, skip_comment=True):
         while True:
             self.tok = self.src[self.cursor]
             self.pos = self.cursor
@@ -184,7 +321,13 @@ class QAPISchemaParser(object):
             self.val = None
 
             if self.tok == '#':
+                if self.src[self.cursor] == '#':
+                    # Start of doc comment
+                    skip_comment = False
                 self.cursor = self.src.find('\n', self.cursor)
+                if not skip_comment:
+                    self.val = self.src[self.pos:self.cursor]
+                    return
             elif self.tok in "{}:,[]":
                 return
             elif self.tok == "'":
@@ -713,7 +856,7 @@ def check_keys(expr_elem, meta, required, optional=[]):
                                 % (key, meta, name))
 
 
-def check_exprs(exprs):
+def check_exprs(exprs, strict_doc):
     global all_names
 
     # Learn the types and check for valid expression keys
@@ -722,6 +865,11 @@ def check_exprs(exprs):
     for expr_elem in exprs:
         expr = expr_elem['expr']
         info = expr_elem['info']
+
+        if strict_doc and 'doc' not in expr_elem:
+            raise QAPILineError(info,
+                                "Expression missing documentation comment")
+
         if 'enum' in expr:
             check_keys(expr_elem, 'enum', ['data'], ['prefix'])
             add_enum(expr['enum'], info, expr['data'])
@@ -780,6 +928,63 @@ def check_exprs(exprs):
     return exprs
 
 
+def check_simple_doc(doc):
+    if doc.symbol:
+        raise QAPILineError(doc.info,
+                            "'%s' documention is not followed by the 
definition"
+                            % doc.symbol)
+
+    body = str(doc.body)
+    if re.search(r'@\S+:', body, re.MULTILINE):
+        raise QAPILineError(doc.info,
+                            "Document body cannot contain @NAME: sections")
+
+
+def check_expr_doc(doc, expr, info):
+    for i in ('enum', 'union', 'alternate', 'struct', 'command', 'event'):
+        if i in expr:
+            meta = i
+            break
+
+    name = expr[meta]
+    if doc.symbol != name:
+        raise QAPILineError(info, "Definition of '%s' follows documentation"
+                            " for '%s'" % (name, doc.symbol))
+    if doc.has_meta('Returns') and 'command' not in expr:
+        raise QAPILineError(info, "Invalid return documentation")
+
+    doc_args = set(doc.args.keys())
+    if meta == 'union':
+        data = expr.get('base', [])
+    else:
+        data = expr.get('data', [])
+    if isinstance(data, dict):
+        data = data.keys()
+    args = set([name.strip('*') for name in data])
+    if meta == 'alternate' or \
+       (meta == 'union' and not expr.get('discriminator')):
+        args.add('type')
+    if not doc_args.issubset(args):
+        raise QAPILineError(info, "Members documentation is not a subset of"
+                            " API %r > %r" % (list(doc_args), list(args)))
+
+
+def check_docs(docs):
+    for doc in docs:
+        for section in doc.args.values() + doc.meta:
+            content = str(section)
+            if not content or content.isspace():
+                raise QAPILineError(doc.info,
+                                    "Empty doc section '%s'" % section.name)
+
+        if not doc.expr:
+            check_simple_doc(doc)
+        else:
+            check_expr_doc(doc, doc.expr, doc.info)
+
+    return docs
+
+
 #
 # Schema compiler frontend
 #
@@ -1249,9 +1454,11 @@ class QAPISchemaEvent(QAPISchemaEntity):
 
 
 class QAPISchema(object):
-    def __init__(self, fname):
+    def __init__(self, fname, strict_doc=False):
         try:
-            self.exprs = check_exprs(QAPISchemaParser(open(fname, "r")).exprs)
+            parser = QAPISchemaParser(open(fname, "r"))
+            self.exprs = check_exprs(parser.exprs, strict_doc)
+            self.docs = check_docs(parser.docs)
             self._entity_dict = {}
             self._predefining = True
             self._def_predefineds()
diff --git a/scripts/qapi2texi.py b/scripts/qapi2texi.py
new file mode 100755
index 0000000..0cec43a
--- /dev/null
+++ b/scripts/qapi2texi.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python
+# QAPI texi generator
+#
+# This work is licensed under the terms of the GNU LGPL, version 2+.
+# See the COPYING file in the top-level directory.
+"""This script produces the documentation of a qapi schema in texinfo format"""
+import re
+import sys
+
+import qapi
+
+COMMAND_FMT = """
address@hidden {type} {{{ret}}} {name} @
+{{{args}}}
+
+{body}
+
address@hidden deftypefn
+
+""".format
+
+ENUM_FMT = """
address@hidden Enum {name}
+
+{body}
+
address@hidden deftp
+
+""".format
+
+STRUCT_FMT = """
address@hidden {{{type}}} {name} @
+{{{attrs}}}
+
+{body}
+
address@hidden deftp
+
+""".format
+
+EXAMPLE_FMT = """@example
+{code}
address@hidden example
+""".format
+
+
+def subst_strong(doc):
+    """Replaces *foo* by @strong{foo}"""
+    return re.sub(r'\*([^_\n]+)\*', r'@emph{\1}', doc)
+
+
+def subst_emph(doc):
+    """Replaces _foo_ by @emph{foo}"""
+    return re.sub(r'\s_([^_\n]+)_\s', r' @emph{\1} ', doc)
+
+
+def subst_vars(doc):
+    """Replaces @var by @code{var}"""
+    return re.sub(r'@([\w-]+)', r'@code{\1}', doc)
+
+
+def subst_braces(doc):
+    """Replaces {} with @{ @}"""
+    return doc.replace("{", "@{").replace("}", "@}")
+
+
+def texi_example(doc):
+    """Format @example"""
+    doc = subst_braces(doc).strip('\n')
+    return EXAMPLE_FMT(code=doc)
+
+
+def texi_comment(doc):
+    """
+    Format a comment
+
+    Lines starting with:
+    - |: generates an @example
+    - =: generates @section
+    - ==: generates @subsection
+    - 1. or 1): generates an @enumerate @item
+    - o/*/-: generates an @itemize list
+    """
+    lines = []
+    doc = subst_braces(doc)
+    doc = subst_vars(doc)
+    doc = subst_emph(doc)
+    doc = subst_strong(doc)
+    inlist = ""
+    lastempty = False
+    for line in doc.split('\n'):
+        empty = line == ""
+
+        if line.startswith("| "):
+            line = EXAMPLE_FMT(code=line[2:])
+        elif line.startswith("= "):
+            line = "@section " + line[2:]
+        elif line.startswith("== "):
+            line = "@subsection " + line[3:]
+        elif re.match("^([0-9]*[.)]) ", line):
+            if not inlist:
+                lines.append("@enumerate")
+                inlist = "enumerate"
+            line = line[line.find(" ")+1:]
+            lines.append("@item")
+        elif re.match("^[o*-] ", line):
+            if not inlist:
+                lines.append("@itemize %s" % {'o': "@bullet",
+                                              '*': "@minus",
+                                              '-': ""}[line[0]])
+                inlist = "itemize"
+            lines.append("@item")
+            line = line[2:]
+        elif lastempty and inlist:
+            lines.append("@end %s\n" % inlist)
+            inlist = ""
+
+        lastempty = empty
+        lines.append(line)
+
+    if inlist:
+        lines.append("@end %s\n" % inlist)
+    return "\n".join(lines)
+
+
+def texi_args(expr, key="data"):
+    """
+    Format the functions/structure/events.. arguments/members
+    """
+    if key not in expr:
+        return ""
+
+    args = expr[key]
+    arg_list = []
+    if isinstance(args, str):
+        arg_list.append(args)
+    else:
+        for name, typ in args.iteritems():
+            # optional arg
+            if name.startswith("*"):
+                name = name[1:]
+                arg_list.append("['%s': @var{%s}]" % (name, typ))
+            # regular arg
+            else:
+                arg_list.append("'%s': @var{%s}" % (name, typ))
+
+    return ", ".join(arg_list)
+
+
+def texi_body(doc):
+    """
+    Format the body of a symbol documentation:
+    - a table of arguments
+    - followed by "Returns/Notes/Since/Example" sections
+    """
+    def _section_order(section):
+        return {"Returns": 0,
+                "Note": 1,
+                "Notes": 1,
+                "Since": 2,
+                "Example": 3,
+                "Examples": 3}[section]
+
+    body = "@table @asis\n"
+    for arg, section in doc.args.iteritems():
+        desc = str(section)
+        opt = ''
+        if desc.startswith("#optional"):
+            desc = desc[10:]
+            opt = ' *'
+        elif desc.endswith("#optional"):
+            desc = desc[:-10]
+            opt = ' *'
+        body += "@item @code{'%s'}%s\n%s\n" % (arg, opt, texi_comment(desc))
+    body += "@end table\n"
+    body += texi_comment(str(doc.body))
+
+    meta = sorted(doc.meta, key=lambda i: _section_order(i.name))
+    for section in meta:
+        name, doc = (section.name, str(section))
+        func = texi_comment
+        if name.startswith("Example"):
+            func = texi_example
+
+        body += "address@hidden address@hidden quotation" % \
+                (name, func(doc))
+    return body
+
+
+def texi_alternate(expr, doc):
+    """
+    Format an alternate to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    return STRUCT_FMT(type="Alternate",
+                      name=doc.symbol,
+                      attrs="[ " + args + " ]",
+                      body=body)
+
+
+def texi_union(expr, doc):
+    """
+    Format an union to texi
+    """
+    attrs = "@{ " + texi_args(expr, "base") + " @}"
+    discriminator = expr.get("discriminator")
+    if not discriminator:
+        union = "Flat Union"
+        discriminator = "type"
+    else:
+        union = "Union"
+    attrs += " + '%s' = [ " % discriminator
+    attrs += texi_args(expr, "data") + " ]"
+    body = texi_body(doc)
+
+    return STRUCT_FMT(type=union,
+                      name=doc.symbol,
+                      attrs=attrs,
+                      body=body)
+
+
+def texi_enum(expr, doc):
+    """
+    Format an enum to texi
+    """
+    for i in expr['data']:
+        if i not in doc.args:
+            doc.args[i] = ''
+    body = texi_body(doc)
+    return ENUM_FMT(name=doc.symbol,
+                    body=body)
+
+
+def texi_struct(expr, doc):
+    """
+    Format a struct to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    attrs = "@{ " + args + " @}"
+    base = expr.get("base")
+    if base:
+        attrs += " + %s" % base
+    return STRUCT_FMT(type="Struct",
+                      name=doc.symbol,
+                      attrs=attrs,
+                      body=body)
+
+
+def texi_command(expr, doc):
+    """
+    Format a command to texi
+    """
+    args = texi_args(expr)
+    ret = expr["returns"] if "returns" in expr else ""
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Command",
+                       name=doc.symbol,
+                       ret=ret,
+                       args="(" + args + ")",
+                       body=body)
+
+
+def texi_event(expr, doc):
+    """
+    Format an event to texi
+    """
+    args = texi_args(expr)
+    body = texi_body(doc)
+    return COMMAND_FMT(type="Event",
+                       name=doc.symbol,
+                       ret="",
+                       args="(" + args + ")",
+                       body=body)
+
+
+def texi_expr(expr, doc):
+    """
+    Format an expr to texi
+    """
+    (kind, _) = expr.items()[0]
+
+    fmt = {"command": texi_command,
+           "struct": texi_struct,
+           "enum": texi_enum,
+           "union": texi_union,
+           "alternate": texi_alternate,
+           "event": texi_event}
+    try:
+        fmt = fmt[kind]
+    except KeyError:
+        raise ValueError("Unknown expression kind '%s'" % kind)
+
+    return fmt(expr, doc)
+
+
+def texi(docs):
+    """
+    Convert QAPI schema expressions to texi documentation
+    """
+    res = []
+    for doc in docs:
+        expr = doc.expr
+        if not expr:
+            res.append(texi_body(doc))
+            continue
+        try:
+            doc = texi_expr(expr, doc)
+            res.append(doc)
+        except:
+            print >>sys.stderr, "error at @%s" % doc.info
+            raise
+
+    return '\n'.join(res)
+
+
+def main(argv):
+    """
+    Takes schema argument, prints result to stdout
+    """
+    if len(argv) != 2:
+        print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0]
+        sys.exit(1)
+
+    schema = qapi.QAPISchema(argv[1], strict_doc=True)
+    print texi(schema.docs)
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/docs/qapi-code-gen.txt b/docs/qapi-code-gen.txt
index 2841c51..8bc963e 100644
--- a/docs/qapi-code-gen.txt
+++ b/docs/qapi-code-gen.txt
@@ -45,16 +45,13 @@ QAPI parser does not).  At present, there is no place where 
a QAPI
 schema requires the use of JSON numbers or null.
 
 Comments are allowed; anything between an unquoted # and the following
-newline is ignored.  Although there is not yet a documentation
-generator, a form of stylized comments has developed for consistently
-documenting details about an expression and when it was added to the
-schema.  The documentation is delimited between two lines of ##, then
-the first line names the expression, an optional overview is provided,
-then individual documentation about each member of 'data' is provided,
-and finally, a 'Since: x.y.z' tag lists the release that introduced
-the expression.  Optional members are tagged with the phrase
-'#optional', often with their default value; and extensions added
-after the expression was first released are also given a '(since
+newline is ignored.  The documentation is delimited between two lines
+of ##, then the first line names the expression, an optional overview
+is provided, then individual documentation about each member of 'data'
+is provided, and finally, a 'Since: x.y.z' tag lists the release that
+introduced the expression.  Optional members are tagged with the
+phrase '#optional', often with their default value; and extensions
+added after the expression was first released are also given a '(since
 x.y.z)' comment.  For example:
 
     ##
@@ -73,12 +70,49 @@ x.y.z)' comment.  For example:
     #           (Since 2.0)
     #
     # Since: 0.14.0
+    #
+    # Notes: You can also make a list:
+    #        - with items
+    #        - like this
+    #
+    # Example:
+    #
+    # -> { "execute": ... }
+    # <- { "return": ... }
+    #
     ##
     { 'struct': 'BlockStats',
       'data': {'*device': 'str', 'stats': 'BlockDeviceStats',
                '*parent': 'BlockStats',
                '*backing': 'BlockStats'} }
 
+It's also possible to create documentation sections, such as:
+
+    ##
+    # = Section
+    # == Subsection
+    #
+    # Some text foo with *strong* and _emphasis_
+    # 1. with a list
+    # 2. like that
+    #
+    # And some code:
+    # | $ echo foo
+    # | -> do this
+    # | <- get that
+    #
+    ##
+
+Text *foo* and _foo_ are for "strong" and "emphasis" styles (they do
+not work over multiple lines). @foo is used to reference a symbol.
+
+Lines starting with the following characters and a space:
+- | are examples
+- = are top section
+- == are subsection
+- X. or X) are enumerations (X is any number)
+- o/*/- are itemized list
+
 The schema sets up a series of types, as well as commands and events
 that will use those types.  Forward references are allowed: the parser
 scans in two passes, where the first pass learns all type names, and
diff --git a/tests/qapi-schema/doc-bad-args.err 
b/tests/qapi-schema/doc-bad-args.err
new file mode 100644
index 0000000..a55e003
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-args.json:1: Members documentation is not a subset 
of API ['a', 'b'] > ['a']
diff --git a/tests/qapi-schema/doc-bad-args.exit 
b/tests/qapi-schema/doc-bad-args.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-bad-args.json 
b/tests/qapi-schema/doc-bad-args.json
new file mode 100644
index 0000000..26992ea
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-args.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+# @a: a
+# @b: b
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-bad-args.out 
b/tests/qapi-schema/doc-bad-args.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-bad-symbol.err 
b/tests/qapi-schema/doc-bad-symbol.err
new file mode 100644
index 0000000..9c969d1
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-bad-symbol.json:1: Definition of 'foo' follows 
documentation for 'food'
diff --git a/tests/qapi-schema/doc-bad-symbol.exit 
b/tests/qapi-schema/doc-bad-symbol.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-bad-symbol.json 
b/tests/qapi-schema/doc-bad-symbol.json
new file mode 100644
index 0000000..7255152
--- /dev/null
+++ b/tests/qapi-schema/doc-bad-symbol.json
@@ -0,0 +1,4 @@
+##
+# @food:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-bad-symbol.out 
b/tests/qapi-schema/doc-bad-symbol.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-arg.err 
b/tests/qapi-schema/doc-duplicated-arg.err
new file mode 100644
index 0000000..88a272b
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-arg.json:4:1: 'a' arg duplicated
diff --git a/tests/qapi-schema/doc-duplicated-arg.exit 
b/tests/qapi-schema/doc-duplicated-arg.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-arg.json 
b/tests/qapi-schema/doc-duplicated-arg.json
new file mode 100644
index 0000000..f7804c2
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-arg.json
@@ -0,0 +1,5 @@
+##
+# @foo:
+# @a:
+# @a:
+##
diff --git a/tests/qapi-schema/doc-duplicated-arg.out 
b/tests/qapi-schema/doc-duplicated-arg.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-return.err 
b/tests/qapi-schema/doc-duplicated-return.err
new file mode 100644
index 0000000..1b02880
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-return.json:5:1: Duplicated 'Returns' section
diff --git a/tests/qapi-schema/doc-duplicated-return.exit 
b/tests/qapi-schema/doc-duplicated-return.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-return.json 
b/tests/qapi-schema/doc-duplicated-return.json
new file mode 100644
index 0000000..de7234b
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-return.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Returns: 0
+# Returns: 1
+##
diff --git a/tests/qapi-schema/doc-duplicated-return.out 
b/tests/qapi-schema/doc-duplicated-return.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-duplicated-since.err 
b/tests/qapi-schema/doc-duplicated-since.err
new file mode 100644
index 0000000..72efb2a
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-duplicated-since.json:5:1: Duplicated 'Since' section
diff --git a/tests/qapi-schema/doc-duplicated-since.exit 
b/tests/qapi-schema/doc-duplicated-since.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-duplicated-since.json 
b/tests/qapi-schema/doc-duplicated-since.json
new file mode 100644
index 0000000..da261a1
--- /dev/null
+++ b/tests/qapi-schema/doc-duplicated-since.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Since: 0
+# Since: 1
+##
diff --git a/tests/qapi-schema/doc-duplicated-since.out 
b/tests/qapi-schema/doc-duplicated-since.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-arg.err 
b/tests/qapi-schema/doc-empty-arg.err
new file mode 100644
index 0000000..0647eed
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-arg.json:3:1: Invalid argument name
diff --git a/tests/qapi-schema/doc-empty-arg.exit 
b/tests/qapi-schema/doc-empty-arg.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-arg.json 
b/tests/qapi-schema/doc-empty-arg.json
new file mode 100644
index 0000000..74f526c
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-arg.json
@@ -0,0 +1,4 @@
+##
+# @foo:
+# @:
+##
diff --git a/tests/qapi-schema/doc-empty-arg.out 
b/tests/qapi-schema/doc-empty-arg.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-section.err 
b/tests/qapi-schema/doc-empty-section.err
new file mode 100644
index 0000000..c940332
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-section.json:1: Empty doc section 'Note'
diff --git a/tests/qapi-schema/doc-empty-section.exit 
b/tests/qapi-schema/doc-empty-section.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-section.json 
b/tests/qapi-schema/doc-empty-section.json
new file mode 100644
index 0000000..ed3867d
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-section.json
@@ -0,0 +1,6 @@
+##
+# @foo:
+#
+# Note:
+##
+{ 'command': 'foo', 'data': {'a': 'int'} }
diff --git a/tests/qapi-schema/doc-empty-section.out 
b/tests/qapi-schema/doc-empty-section.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-empty-symbol.err 
b/tests/qapi-schema/doc-empty-symbol.err
new file mode 100644
index 0000000..955dc3c
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-empty-symbol.json:2:1: Invalid symbol
diff --git a/tests/qapi-schema/doc-empty-symbol.exit 
b/tests/qapi-schema/doc-empty-symbol.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-empty-symbol.json 
b/tests/qapi-schema/doc-empty-symbol.json
new file mode 100644
index 0000000..8a2d662
--- /dev/null
+++ b/tests/qapi-schema/doc-empty-symbol.json
@@ -0,0 +1,3 @@
+##
+# @:
+##
diff --git a/tests/qapi-schema/doc-empty-symbol.out 
b/tests/qapi-schema/doc-empty-symbol.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-end.err 
b/tests/qapi-schema/doc-invalid-end.err
new file mode 100644
index 0000000..5ed8911
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-end.json:3:2: Documentation comment must end 
with '##'
diff --git a/tests/qapi-schema/doc-invalid-end.exit 
b/tests/qapi-schema/doc-invalid-end.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-end.json 
b/tests/qapi-schema/doc-invalid-end.json
new file mode 100644
index 0000000..7452718
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end.json
@@ -0,0 +1,3 @@
+##
+# An invalid comment
+#
diff --git a/tests/qapi-schema/doc-invalid-end.out 
b/tests/qapi-schema/doc-invalid-end.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-end2.err 
b/tests/qapi-schema/doc-invalid-end2.err
new file mode 100644
index 0000000..acd23bb
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-end2.json:3:1: Junk after '##' at end of 
documentation comment
diff --git a/tests/qapi-schema/doc-invalid-end2.exit 
b/tests/qapi-schema/doc-invalid-end2.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-end2.json 
b/tests/qapi-schema/doc-invalid-end2.json
new file mode 100644
index 0000000..12e669e
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-end2.json
@@ -0,0 +1,3 @@
+##
+#
+## invalid
diff --git a/tests/qapi-schema/doc-invalid-end2.out 
b/tests/qapi-schema/doc-invalid-end2.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-return.err 
b/tests/qapi-schema/doc-invalid-return.err
new file mode 100644
index 0000000..c3f2691
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-return.json:1: Invalid return documentation
diff --git a/tests/qapi-schema/doc-invalid-return.exit 
b/tests/qapi-schema/doc-invalid-return.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-return.json 
b/tests/qapi-schema/doc-invalid-return.json
new file mode 100644
index 0000000..6568b6d
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-return.json
@@ -0,0 +1,5 @@
+##
+# @foo:
+# Returns: blah
+##
+{ 'event': 'foo' }
diff --git a/tests/qapi-schema/doc-invalid-return.out 
b/tests/qapi-schema/doc-invalid-return.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-section.err 
b/tests/qapi-schema/doc-invalid-section.err
new file mode 100644
index 0000000..f4c12aa
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-section.json:1: Document body cannot contain 
@NAME: sections
diff --git a/tests/qapi-schema/doc-invalid-section.exit 
b/tests/qapi-schema/doc-invalid-section.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-section.json 
b/tests/qapi-schema/doc-invalid-section.json
new file mode 100644
index 0000000..9afa2f1
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-section.json
@@ -0,0 +1,4 @@
+##
+# freeform
+# @note: foo
+##
diff --git a/tests/qapi-schema/doc-invalid-section.out 
b/tests/qapi-schema/doc-invalid-section.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-invalid-start.err 
b/tests/qapi-schema/doc-invalid-start.err
new file mode 100644
index 0000000..194c8d7
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-invalid-start.json:1:1: Junk after '##' at start of 
documentation comment
diff --git a/tests/qapi-schema/doc-invalid-start.exit 
b/tests/qapi-schema/doc-invalid-start.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-invalid-start.json 
b/tests/qapi-schema/doc-invalid-start.json
new file mode 100644
index 0000000..f85adfd
--- /dev/null
+++ b/tests/qapi-schema/doc-invalid-start.json
@@ -0,0 +1,3 @@
+## invalid
+#
+##
diff --git a/tests/qapi-schema/doc-invalid-start.out 
b/tests/qapi-schema/doc-invalid-start.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-missing-expr.err 
b/tests/qapi-schema/doc-missing-expr.err
new file mode 100644
index 0000000..e4ed135
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-missing-expr.json:1: 'bar' documention is not followed 
by the definition
diff --git a/tests/qapi-schema/doc-missing-expr.exit 
b/tests/qapi-schema/doc-missing-expr.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-missing-expr.json 
b/tests/qapi-schema/doc-missing-expr.json
new file mode 100644
index 0000000..a0be2e1
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-expr.json
@@ -0,0 +1,3 @@
+##
+# @bar:
+##
diff --git a/tests/qapi-schema/doc-missing-expr.out 
b/tests/qapi-schema/doc-missing-expr.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/doc-missing-space.err 
b/tests/qapi-schema/doc-missing-space.err
new file mode 100644
index 0000000..37056ce
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.err
@@ -0,0 +1 @@
+tests/qapi-schema/doc-missing-space.json:3:1: missing space after #
diff --git a/tests/qapi-schema/doc-missing-space.exit 
b/tests/qapi-schema/doc-missing-space.exit
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.exit
@@ -0,0 +1 @@
+1
diff --git a/tests/qapi-schema/doc-missing-space.json 
b/tests/qapi-schema/doc-missing-space.json
new file mode 100644
index 0000000..39303de
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-space.json
@@ -0,0 +1,4 @@
+##
+# missing space:
+#wef
+##
diff --git a/tests/qapi-schema/doc-missing-space.out 
b/tests/qapi-schema/doc-missing-space.out
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qapi-schema/qapi-schema-test.json 
b/tests/qapi-schema/qapi-schema-test.json
index 1719463..d921ec4 100644
--- a/tests/qapi-schema/qapi-schema-test.json
+++ b/tests/qapi-schema/qapi-schema-test.json
@@ -3,6 +3,43 @@
 # This file is a stress test of supported qapi constructs that must
 # parse and compile correctly.
 
+##
+# = Section
+# == subsection
+#
+# Some text foo with *strong* and _emphasis_
+# 1. with a list
+# 2. like that @foo
+#
+# And some code:
+# | $ echo foo
+# | -> do this
+# | <- get that
+#
+# Note: is not a meta
+##
+
+##
+# @TestStruct:
+# @integer: foo
+#           blah
+#
+#           bao
+#
+# @boolean: bar
+# @string: baz
+#
+# body with @var
+#
+# Example:
+#
+# -> { "execute": ... }
+# <- { "return": ... }
+#
+# Since: 2.3
+# Note: a note
+#
+##
 { 'struct': 'TestStruct',
   'data': { 'integer': 'int', 'boolean': 'bool', 'string': 'str' } }
 
@@ -123,6 +160,27 @@
   'data': {'ud1a': 'UserDefOne', '*ud1b': 'UserDefOne'},
   'returns': 'UserDefTwo' }
 
+##
+# Another comment
+##
+
+##
+# @guest-get-time:
+#
+# @a: an integer
+# @b: #optional integer
+#
+# @guest-get-time body
+#
+# Returns: returns something
+#
+# Example:
+#
+# -> { "execute": "guest-get-time", ... }
+# <- { "return": "42" }
+#
+##
+
 # Returning a non-dictionary requires a name from the whitelist
 { 'command': 'guest-get-time', 'data': {'a': 'int', '*b': 'int' },
   'returns': 'int' }
diff --git a/tests/qapi-schema/qapi-schema-test.out 
b/tests/qapi-schema/qapi-schema-test.out
index 9d99c4e..fde9e06 100644
--- a/tests/qapi-schema/qapi-schema-test.out
+++ b/tests/qapi-schema/qapi-schema-test.out
@@ -232,3 +232,52 @@ command user_def_cmd1 q_obj_user_def_cmd1-arg -> None
    gen=True success_response=True boxed=False
 command user_def_cmd2 q_obj_user_def_cmd2-arg -> UserDefTwo
    gen=True success_response=True boxed=False
+doc freeform
+    body=
+= Section
+== subsection
+
+Some text foo with *strong* and _emphasis_
+1. with a list
+2. like that @foo
+
+And some code:
+| $ echo foo
+| -> do this
+| <- get that
+
+Note: is not a meta
+doc symbol=TestStruct expr=('struct', 'TestStruct')
+    arg=integer
+foo
+blah
+
+bao
+    arg=boolean
+bar
+    arg=string
+baz
+    meta=Example
+-> { "execute": ... }
+<- { "return": ... }
+    meta=Since
+2.3
+    meta=Note
+a note
+    body=
+body with @var
+doc freeform
+    body=
+Another comment
+doc symbol=guest-get-time expr=('command', 'guest-get-time')
+    arg=a
+an integer
+    arg=b
+#optional integer
+    meta=Returns
+returns something
+    meta=Example
+-> { "execute": "guest-get-time", ... }
+<- { "return": "42" }
+    body=
address@hidden body
diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
index ef74e2c..22da014 100644
--- a/tests/qapi-schema/test-qapi.py
+++ b/tests/qapi-schema/test-qapi.py
@@ -55,3 +55,15 @@ class QAPISchemaTestVisitor(QAPISchemaVisitor):
 
 schema = QAPISchema(sys.argv[1])
 schema.visit(QAPISchemaTestVisitor())
+
+for doc in schema.docs:
+    if doc.symbol:
+        print 'doc symbol=%s expr=%s' % \
+            (doc.symbol, doc.expr.items()[0])
+    else:
+        print 'doc freeform'
+    for arg, section in doc.args.iteritems():
+        print '    arg=%s\n%s' % (arg, section)
+    for section in doc.meta:
+        print '    meta=%s\n%s' % (section.name, section)
+    print '    body=\n%s' % doc.body
-- 
2.10.0




reply via email to

[Prev in Thread] Current Thread [Next in Thread]